@fluid-app/fluid-cli-theme-dev 0.1.55 → 0.1.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["settingTypesJson.types","parseDocument","isRecord","themes.listThemeResources","themes.getThemeAssets","isRecord","themes.createFileResource","themes.destroyFileResource","themes.updateThemeResource","themes.deleteThemeResource","themes.listApplicationThemes","themes.getApplicationTheme","themes.cloneApplicationThemeForDevelopment","themes.createApplicationTheme","themes.getApplicationTheme","themes.createApplicationTheme","themes.getApplicationTheme","themes.publishApplicationTheme","themes.getApplicationThemeAvailableThemeables"],"sources":["../../../platform/api-client-core/src/api-error-shape.ts","../../../platform/api-client-core/src/fetch-client.ts","../src/api.ts","../src/theme-config.ts","../src/plugin-state.ts","../src/theme/mime-type.ts","../src/theme/resource-key.ts","../../../platform/theme-schema/src/setting-types.json","../../../platform/theme-schema/src/types.ts","../../../platform/theme-schema/src/validate-settings.ts","../../../platform/theme-schema/src/validate-blocks.ts","../../../platform/theme-schema/src/validate.ts","../../../platform/theme-schema/src/sections.ts","../src/theme/file.ts","../src/theme/fluid-ignore.ts","../src/theme/root.ts","../src/theme/dev-server/sse.ts","../src/theme/dev-server/hot-reload.ts","../src/theme/dev-server/proxy.ts","../../../api-clients/themes/src/namespaces/v0.ts","../src/theme/format-error.ts","../src/theme/dev-server/watcher.ts","../src/theme/case-collisions.ts","../src/theme/asset-manifest.ts","../src/theme/stylesheet-keys.ts","../src/theme/syncer.ts","../src/theme/liquid-delimiters.ts","../src/theme/dev-server/port-preflight.ts","../src/theme/dev-server/index.ts","../src/theme/dev-remote-baseline.ts","../src/theme/shadow-repo.ts","../src/theme-picker.ts","../src/workspace.ts","../src/commands/dev.ts","../src/theme/merge-push.ts","../src/theme/auto-baseline.ts","../src/theme/legacy-migration.ts","../src/theme/sync-identity.ts","../src/commands/push.ts","../src/theme/merge-pull.ts","../src/commands/pull.ts","../src/commands/lint.ts","../src/commands/init.ts","../src/commands/navigate.ts","../src/skills/install.ts","../src/commands/skills.ts","../src/commands/theme.ts","../src/index.ts"],"sourcesContent":["/**\n * The shape contract for `ApiError.body` and `ApiError.data`.\n *\n * One decision, in one place: `body` is the response envelope exactly as the\n * server sent it, `data` is the unwrapped field-error bag. Rationale and the\n * alternatives considered live in `docs/api-error-data-contract.md`.\n */\n\n/**\n * The parsed JSON error response body, exactly as the server sent it.\n *\n * This is the envelope: `errors`, `error_message`, `error`, `status`, `meta`\n * and `request_id` all live here. `null` when the response carried no JSON\n * object — HTML, an empty body, a JSON primitive, or unparseable text.\n */\nexport type ApiErrorBody = Record<string, unknown> | null;\n\n/**\n * The field-level error bag: the envelope's `errors` value when the body has\n * one, otherwise the body itself.\n *\n * `errors` is declared `never` because unwrapping already consumed it — read\n * the envelope from `ApiError.body`, not from here. That makes the historical\n * mistake (`error.data.errors.some_field`, which silently resolved to\n * `undefined`) a typecheck failure instead of a degraded error message.\n */\nexport type ApiErrorFieldErrors = Record<string, unknown> & {\n errors?: never;\n};\n\n/**\n * Every value `ApiError.data` can hold. Fluid endpoints return an object bag,\n * but `errors` is occasionally an array or a string, and non-JSON responses\n * carry no data at all.\n */\nexport type ApiErrorData =\n | ApiErrorFieldErrors\n | readonly unknown[]\n | string\n | number\n | boolean\n | null;\n\n/**\n * Narrows a parsed JSON error payload to the envelope contract. Arrays and\n * primitives are not envelopes, so they resolve to `null` — their content is\n * still reachable through `ApiError.data`.\n */\nexport function toApiErrorBody(value: unknown): ApiErrorBody {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return null;\n }\n\n return value as Record<string, unknown>;\n}\n\n/**\n * The single unwrap rule, shared by every producer of `ApiError`.\n *\n * `??` rather than `||` so an explicitly empty `errors` (`\"\"`, `0`, `false`)\n * is preserved instead of silently falling back to the whole envelope.\n */\nexport function toApiErrorData(body: ApiErrorBody): ApiErrorData {\n if (!body) {\n return null;\n }\n\n return (body.errors ?? body) as ApiErrorData;\n}\n","/**\n * Minimal, framework-agnostic fetch client for Fluid APIs\n * Compatible with fluid-admin patterns but usable standalone\n */\n\nimport type { ApiErrorBody, ApiErrorData } from \"./api-error-shape\";\nimport { toApiErrorBody, toApiErrorData } from \"./api-error-shape\";\n\nexport interface FetchClientConfig {\n /**\n * Base URL for all requests (e.g., \"https://api.fluid.app/api\")\n */\n baseUrl: string;\n\n /**\n * Optional function to get auth token\n * Return null/undefined if no token available\n */\n getAuthToken?: () => string | null | Promise<string | null>;\n\n /**\n * Optional callback when 401 auth error occurs\n */\n onAuthError?: () => void;\n\n /**\n * Default headers to include in all requests\n * Example: { \"x-fluid-client\": \"admin\" }\n */\n defaultHeaders?: Record<string, string>;\n\n /**\n * Credentials mode for fetch requests.\n * Set to `\"include\"` for cookie-based (same-origin BFF) authentication.\n * @default undefined (browser default: \"same-origin\")\n */\n credentials?: RequestCredentials;\n\n /**\n * Request cache mode for fetch requests.\n * @default undefined (browser default)\n */\n cache?: RequestCache;\n\n /**\n * Retry configuration for thrown network errors from fetch.\n * Does not retry HTTP error responses or aborted requests.\n * @default undefined (no retries)\n */\n networkRetry?: {\n maxRetries?: number;\n baseDelayMs?: number;\n };\n\n /**\n * Throw ApiError when a successful response declares JSON but cannot be parsed.\n * Defaults to false to preserve the legacy generated-client behavior.\n * @default false\n */\n throwOnInvalidJson?: boolean;\n}\n\nexport interface RequestOptions {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\";\n headers?: Record<string, string>;\n params?: Record<string, unknown>;\n body?: unknown;\n signal?: AbortSignal;\n priority?: RequestInit[\"priority\"];\n}\n\n/**\n * API Error class compatible with fluid-admin's ApiError\n */\nexport class ApiError extends Error {\n public readonly status: number;\n\n /**\n * The unwrapped field-error bag. See `ApiErrorData`; the contract is\n * documented in `docs/api-error-data-contract.md`.\n */\n public readonly data: ApiErrorData;\n\n /**\n * The full error response envelope. Read `error_message`, `error`, `status`\n * and nested `errors` from here — `data` has already unwrapped one level.\n */\n public readonly body: ApiErrorBody;\n\n public readonly requestId?: string;\n\n constructor(\n message: string,\n status: number,\n data?: ApiErrorData,\n requestId?: string,\n body?: ApiErrorBody,\n ) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.data = data ?? null;\n this.body = body ?? null;\n this.requestId = requestId;\n\n if (\"captureStackTrace\" in Error) {\n (\n Error as {\n captureStackTrace: (\n target: Error,\n constructor: NewableFunction,\n ) => void;\n }\n ).captureStackTrace(this, ApiError);\n }\n }\n\n toJSON(): {\n name: string;\n message: string;\n status: number;\n data: ApiErrorData;\n body: ApiErrorBody;\n requestId?: string;\n } {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n data: this.data,\n body: this.body,\n requestId: this.requestId,\n };\n }\n}\n\nfunction getStringRequestId(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction getRequestIdFromHeaders(headers: Headers): string | undefined {\n return (\n getStringRequestId(headers.get(\"x-request-id\")) ??\n getStringRequestId(headers.get(\"request-id\")) ??\n getStringRequestId(headers.get(\"X-Request-ID\"))\n );\n}\n\nfunction getRequestIdFromJsonBody(body: unknown): string | undefined {\n if (!body || typeof body !== \"object\" || Array.isArray(body)) {\n return undefined;\n }\n\n const record = body as Record<string, unknown>;\n const meta = record.meta;\n\n return (\n getStringRequestId(record.request_id) ??\n getStringRequestId(record.requestId) ??\n (meta && typeof meta === \"object\" && !Array.isArray(meta)\n ? (getStringRequestId((meta as Record<string, unknown>).request_id) ??\n getStringRequestId((meta as Record<string, unknown>).requestId))\n : undefined)\n );\n}\n\n/**\n * Type guard for ApiError\n */\nexport function isApiError(error: unknown): error is ApiError {\n return error instanceof ApiError;\n}\n\nexport interface FetchClientInstance {\n request: <TResponse = unknown>(\n endpoint: string,\n options?: RequestOptions,\n ) => Promise<TResponse>;\n requestWithFormData: <TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options?: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n },\n ) => Promise<TResponse>;\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ) => Promise<TResponse>;\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ) => Promise<TResponse>;\n}\n\n/**\n * Creates a configured fetch client instance\n */\nexport function createFetchClient(\n config: FetchClientConfig,\n): FetchClientInstance {\n const {\n baseUrl,\n getAuthToken,\n onAuthError,\n defaultHeaders = {},\n credentials,\n cache,\n networkRetry,\n throwOnInvalidJson = false,\n } = config;\n const maxNetworkRetries = Math.max(0, networkRetry?.maxRetries ?? 0);\n const baseNetworkRetryDelayMs = Math.max(0, networkRetry?.baseDelayMs ?? 0);\n\n /**\n * Build headers for a request\n */\n async function buildHeaders(\n customHeaders?: Record<string, string>,\n ): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...defaultHeaders,\n ...customHeaders,\n };\n\n // Add auth token if available\n if (getAuthToken) {\n const token = await getAuthToken();\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n }\n\n return headers;\n }\n\n /**\n * Join baseUrl + endpoint via string concatenation (matches fetchApi).\n * Using `new URL(endpoint, baseUrl)` would strip any path prefix from\n * baseUrl (e.g. \"/api\") when the endpoint starts with \"/\".\n */\n function joinUrl(endpoint: string): string {\n return `${baseUrl}${endpoint}`;\n }\n\n /**\n * Build URL with query parameters for GET requests\n * Compatible with fluid-admin's query param handling\n */\n function buildUrl(\n endpoint: string,\n params?: Record<string, unknown>,\n ): string {\n const fullUrl = joinUrl(endpoint);\n\n if (!params || Object.keys(params).length === 0) {\n return fullUrl;\n }\n\n const queryString = new URLSearchParams();\n\n Object.entries(params).forEach(([key, value]) => {\n if (value === undefined || value === null) {\n return; // Skip undefined/null values\n }\n\n if (Array.isArray(value)) {\n // Handle arrays like Rails expects: key[]\n value.forEach((item) => queryString.append(`${key}[]`, String(item)));\n } else if (typeof value === \"object\") {\n // Handle nested objects: key[subkey]\n Object.entries(value).forEach(([subKey, subValue]) => {\n if (subValue === undefined || subValue === null) {\n return;\n }\n\n if (Array.isArray(subValue)) {\n subValue.forEach((item) =>\n queryString.append(`${key}[${subKey}][]`, String(item)),\n );\n } else {\n queryString.append(`${key}[${subKey}]`, String(subValue));\n }\n });\n } else {\n queryString.append(key, String(value));\n }\n });\n\n const qs = queryString.toString();\n return qs ? `${fullUrl}?${qs}` : fullUrl;\n }\n\n /**\n * Shared response handler for both JSON and FormData requests.\n * Handles auth errors, non-OK responses, 204 No Content, and JSON parsing.\n */\n async function handleResponse<TResponse>(\n response: Response,\n method: string,\n _url: string,\n ): Promise<TResponse> {\n const headerRequestId = getRequestIdFromHeaders(response.headers);\n\n if (response.status === 401 && onAuthError) {\n onAuthError();\n }\n\n if (!response.ok) {\n // Read body as text first to avoid SyntaxError from response.json()\n // when server returns non-JSON bodies with application/json content-type.\n const errorText = await response.text().catch(() => \"\");\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n // Parsed as `unknown` and narrowed before any property read: a body of\n // `null` (or a bare string/number/array) is valid JSON, and reading\n // `.error` off it directly threw a TypeError instead of an ApiError.\n let parsed: unknown;\n try {\n parsed = JSON.parse(errorText);\n } catch {\n throw new ApiError(\n errorText.slice(0, 200) ||\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n headerRequestId,\n );\n }\n\n const body = toApiErrorBody(parsed);\n // Only an object envelope can carry a message. Some Rails BFF endpoints\n // return `{ error: { message, details } }` instead of `{ message }` or\n // `{ error_message }`, so that shape is tried last.\n const msg = body\n ? (() => {\n const nestedError =\n typeof body.error === \"object\" && body.error !== null\n ? (body.error as { message?: unknown }).message\n : undefined;\n const directError =\n typeof body.error === \"string\" ? body.error : undefined;\n const message =\n typeof body.message === \"string\" ? body.message : undefined;\n const errorMessage =\n typeof body.error_message === \"string\"\n ? body.error_message\n : undefined;\n return (\n message ||\n errorMessage ||\n directError ||\n (typeof nestedError === \"string\" ? nestedError : undefined)\n );\n })()\n : undefined;\n\n throw new ApiError(\n msg || `${method} request failed with status ${response.status}`,\n response.status,\n body ? toApiErrorData(body) : (parsed as ApiErrorData),\n headerRequestId ?? getRequestIdFromJsonBody(parsed),\n body,\n );\n } else {\n throw new ApiError(\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n headerRequestId,\n );\n }\n }\n\n if (\n response.status === 204 ||\n response.headers.get(\"content-length\") === \"0\"\n ) {\n return null as TResponse;\n }\n\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n const responseText = await response.text();\n\n try {\n const data = JSON.parse(responseText);\n return data as TResponse;\n } catch {\n if (throwOnInvalidJson) {\n throw new ApiError(\n \"Failed to parse response as JSON\",\n response.status,\n null,\n headerRequestId,\n );\n }\n\n // API declared JSON content-type but body isn't valid JSON.\n // Return the raw payload to preserve the legacy non-strict path.\n return responseText ? (responseText as TResponse) : (null as TResponse);\n }\n }\n\n // Non-JSON response (text/plain, text/html, etc.)\n return null as TResponse;\n }\n\n function getNetworkRetryDelayMs(retryAttempt: number): number {\n return baseNetworkRetryDelayMs * 2 ** (retryAttempt - 1);\n }\n\n async function waitForNetworkRetry(retryAttempt: number): Promise<void> {\n const delayMs = getNetworkRetryDelayMs(retryAttempt);\n if (delayMs <= 0) {\n return;\n }\n\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n\n async function fetchWithNetworkRetry(\n url: string,\n fetchOptions: RequestInit,\n signal?: AbortSignal,\n ): Promise<Response> {\n let retryCount = 0;\n\n while (true) {\n try {\n return await fetch(url, fetchOptions);\n } catch (networkError) {\n if (signal?.aborted || retryCount >= maxNetworkRetries) {\n throw networkError;\n }\n\n retryCount += 1;\n await waitForNetworkRetry(retryCount);\n\n if (signal?.aborted) {\n throw networkError;\n }\n }\n }\n }\n\n /**\n * Main request function\n */\n async function request<TResponse = unknown>(\n endpoint: string,\n options: RequestOptions = {},\n ): Promise<TResponse> {\n const {\n method = \"GET\",\n headers: customHeaders,\n params,\n body,\n signal,\n priority,\n } = options;\n\n const url = params ? buildUrl(endpoint, params) : joinUrl(endpoint);\n\n const headers = await buildHeaders(customHeaders);\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers };\n if (credentials) fetchOptions.credentials = credentials;\n if (cache) fetchOptions.cache = cache;\n if (priority) fetchOptions.priority = priority;\n const serializedBody =\n body && method !== \"GET\" ? JSON.stringify(body) : null;\n if (serializedBody) fetchOptions.body = serializedBody;\n if (signal) fetchOptions.signal = signal;\n response = await fetchWithNetworkRetry(url, fetchOptions, signal);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n /**\n * Request with FormData (for file uploads)\n */\n async function requestWithFormData<TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n } = {},\n ): Promise<TResponse> {\n const {\n method = \"POST\",\n headers: customHeaders,\n signal,\n priority,\n } = options;\n\n const url = joinUrl(endpoint);\n const headers = await buildHeaders(customHeaders);\n\n // Remove Content-Type to let browser set it with boundary\n delete headers[\"Content-Type\"];\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers, body: formData };\n if (credentials) fetchOptions.credentials = credentials;\n if (cache) fetchOptions.cache = cache;\n if (priority) fetchOptions.priority = priority;\n if (signal) fetchOptions.signal = signal;\n response = await fetchWithNetworkRetry(url, fetchOptions, signal);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n // Return client with convenience methods\n return {\n request: request,\n requestWithFormData: requestWithFormData,\n\n // Convenience methods for common HTTP verbs\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"GET\" as const,\n ...(params && { params }),\n }),\n\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"POST\",\n body,\n }),\n\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PUT\",\n body,\n }),\n\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PATCH\",\n body,\n }),\n\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"DELETE\",\n }),\n };\n}\n\nexport type FetchClient = FetchClientInstance;\n","import {\n createFetchClient,\n type FetchClient,\n} from \"@fluid-app/api-client-core\";\nimport { getAuthToken } from \"@fluid-app/fluid-cli\";\n\nexport type ApiClient = FetchClient;\n\n/** Base URL for all API calls. Set FLUID_API_BASE to route through a BFF. */\nfunction getApiBase(): string {\n return process.env[\"FLUID_API_BASE\"] ?? \"https://api.fluid.app\";\n}\n\nexport function createApiClient(tokenOverride?: string): ApiClient {\n return createFetchClient({\n baseUrl: getApiBase(),\n getAuthToken: () => tokenOverride ?? getAuthToken() ?? null,\n });\n}\n\nexport function requireToken(): string {\n const token = getAuthToken();\n if (!token) {\n console.error(\"Not logged in. Run `fluid login` first.\");\n process.exit(1);\n }\n return token;\n}\n","import { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport interface ThemeConfig {\n themeId: number;\n themeName: string;\n company: string;\n /**\n * Server's `content_version_sha` at the moment of the last pull.\n * Sent back to the server as `base_sha` on push — the server rejects\n * with 409 if it no longer matches, so a push against stale data\n * can't silently clobber. Optional so a `.fluid-theme.json` written\n * by an older CLI (checksums-era) still deserializes; when absent,\n * the CLI falls back to the pre-merge-aware behavior (no server-side\n * check).\n */\n baseSha?: string;\n /** Digest of the managed ImageKit references captured with this baseline. */\n assetManifestSha?: string;\n}\n\n/**\n * Shape of a `.fluid-theme.json` written by the pre-shadow-repo CLI.\n * We read it only to migrate the shadow repo forward on first\n * new-CLI use; we never write this shape back.\n */\nexport interface LegacyThemeConfig extends ThemeConfig {\n lastPulledAt?: string | null;\n checksums?: Record<string, string>;\n}\n\nconst CONFIG_FILE = \".fluid-theme.json\";\n\n/**\n * `company` must be the bare subdomain slug — the dev server builds\n * `<company>.fluid.app` from it, so a full domain here produces an\n * unreachable `<slug>.fluid.app.fluid.app` host. Some external tooling\n * writes the full domain (or a URL); accept those and reduce them to\n * the subdomain.\n */\nfunction normalizeCompany(company: string): string {\n const host = company\n .replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, \"\")\n .replace(/[/?#].*$/, \"\");\n return host.replace(/\\.fluid\\.app$/i, \"\");\n}\n\nfunction configPath(themeRoot: string): string {\n return join(themeRoot, CONFIG_FILE);\n}\n\n/** Read `.fluid-theme.json` from a theme directory, or null if it doesn't exist. */\nexport function readThemeConfig(themeRoot: string): ThemeConfig | null {\n const path = configPath(themeRoot);\n if (!existsSync(path)) return null;\n try {\n const raw = readFileSync(path, \"utf-8\");\n const config = JSON.parse(raw) as ThemeConfig;\n if (typeof config.company === \"string\") {\n config.company = normalizeCompany(config.company);\n }\n return config;\n } catch {\n return null;\n }\n}\n\n/**\n * Read a legacy config (pre-shadow-repo). Used only by the migration\n * path on first new-CLI pull to seed shadow HEAD from files whose\n * local content still matches their stored sha256 checksum.\n */\nexport function readLegacyThemeConfig(\n themeRoot: string,\n): LegacyThemeConfig | null {\n const path = configPath(themeRoot);\n if (!existsSync(path)) return null;\n try {\n const raw = readFileSync(path, \"utf-8\");\n return JSON.parse(raw) as LegacyThemeConfig;\n } catch {\n return null;\n }\n}\n\n/** Write `.fluid-theme.json` to a theme directory. */\nexport function writeThemeConfig(themeRoot: string, config: ThemeConfig): void {\n const path = configPath(themeRoot);\n writeFileSync(path, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n","import { existsSync } from \"node:fs\";\nimport { readConfig, updateConfig } from \"@fluid-app/fluid-cli\";\n\nexport interface DevThemeRef {\n id: number;\n name: string;\n /** Theme that was pulled when this isolated dev target was created. */\n sourceThemeId?: number;\n}\n\ninterface ThemeDevState {\n /**\n * Dev themes keyed per project, so `theme dev` in one working copy never\n * reuses (and clobbers) another project's sandbox theme. See `devThemeKey`.\n * Entries are pruned once their theme directory no longer exists, so the map\n * can't grow without bound as projects (and one-off/temp dirs) come and go.\n */\n devThemes?: Record<string, DevThemeRef>;\n /** Most recently started dev theme — `navigate`'s default target. */\n lastDevThemeId?: number;\n /**\n * Legacy single global dev theme id. Older CLI versions stored one dev theme\n * here regardless of project. Read once for migration (see `getDevTheme`),\n * then dropped in favour of `devThemes`.\n */\n devThemeId?: number;\n /** Legacy companion to `devThemeId`. */\n devThemeName?: string;\n}\n\nconst PLUGIN_KEY = \"theme-dev\";\n\nfunction getState(): ThemeDevState {\n const config = readConfig();\n return (config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {};\n}\n\n/** Extract the absolute theme root from a `company:themeRoot` key. */\nfunction themeRootFromKey(key: string): string {\n const sep = key.indexOf(\":\");\n return sep === -1 ? key : key.slice(sep + 1);\n}\n\n/**\n * Set `key` to `theme`, dropping any entries whose theme directory no longer\n * exists. Tying an entry's lifetime to its directory keeps the map bounded —\n * abandoned/deleted projects fall out the next time `theme dev` runs anywhere.\n */\nfunction withDevTheme(\n existing: Record<string, DevThemeRef> | undefined,\n key: string,\n theme: DevThemeRef,\n): Record<string, DevThemeRef> {\n const next: Record<string, DevThemeRef> = {};\n for (const [k, v] of Object.entries(existing ?? {})) {\n if (existsSync(themeRootFromKey(k))) next[k] = v;\n }\n next[key] = theme;\n return next;\n}\n\n/**\n * Stable key identifying a dev theme's owning project: the Fluid company\n * (subdomains are globally unique) plus the absolute theme root. Two working\n * copies — or the same copy pulled from two companies — get distinct keys.\n */\nexport function devThemeKey(\n company: string | undefined,\n themeRoot: string,\n): string {\n return `${company ?? \"default\"}:${themeRoot}`;\n}\n\n/**\n * The dev theme stored for a project key, if any. Falls back once to the legacy\n * global `devThemeId` (older CLI versions) and adopts it for this key — clearing\n * the legacy fields so a second project can't adopt the same theme and collide.\n */\nexport function getDevTheme(key: string): DevThemeRef | undefined {\n const state = getState();\n const existing = state.devThemes?.[key];\n if (existing) return existing;\n\n if (state.devThemeId) {\n const migrated: DevThemeRef = {\n id: state.devThemeId,\n name: state.devThemeName ?? `Development #${state.devThemeId}`,\n };\n updateConfig((config) => {\n const current = (config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {};\n const { devThemeId: _id, devThemeName: _name, ...rest } = current;\n return {\n ...config,\n plugins: {\n ...config.plugins,\n [PLUGIN_KEY]: {\n ...rest,\n devThemes: withDevTheme(rest.devThemes, key, migrated),\n lastDevThemeId: migrated.id,\n },\n },\n };\n });\n return migrated;\n }\n\n return undefined;\n}\n\n/** Store (or refresh) the dev theme for a project key and mark it most-recent. */\nexport function setDevTheme(key: string, theme: DevThemeRef): void {\n updateConfig((config) => {\n const current = (config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {};\n return {\n ...config,\n plugins: {\n ...config.plugins,\n [PLUGIN_KEY]: {\n ...current,\n devThemes: withDevTheme(current.devThemes, key, theme),\n lastDevThemeId: theme.id,\n },\n },\n };\n });\n}\n\n/** Forget a project's dev theme (it was deleted remotely or is no longer a dev theme). */\nexport function clearDevTheme(key: string): void {\n updateConfig((config) => {\n const current = (config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {};\n const removed = current.devThemes?.[key];\n if (!removed) return config;\n const { [key]: _removed, ...rest } = current.devThemes ?? {};\n const next: ThemeDevState = { ...current, devThemes: rest };\n // Don't leave `navigate` pointing at a theme we just forgot.\n if (current.lastDevThemeId === removed.id) {\n next.lastDevThemeId = undefined;\n }\n return {\n ...config,\n plugins: { ...config.plugins, [PLUGIN_KEY]: next },\n };\n });\n}\n\n/**\n * Mark a theme as the most recently started dev server (`navigate`'s default)\n * without recording it as a project's dev theme — used for the `--theme`\n * escape hatch, which may target an arbitrary (non-dev) theme.\n */\nexport function setLastDevThemeId(id: number): void {\n updateConfig((config) => {\n const current = (config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {};\n return {\n ...config,\n plugins: {\n ...config.plugins,\n [PLUGIN_KEY]: { ...current, lastDevThemeId: id },\n },\n };\n });\n}\n\n/**\n * The dev theme to target by default in `navigate` — the most recently started\n * dev server. Falls back to the legacy global id for users who haven't yet run\n * the per-project `theme dev`.\n */\nexport function getLastDevThemeId(): number | undefined {\n const state = getState();\n return state.lastDevThemeId ?? state.devThemeId;\n}\n","const TEXT_TYPES: Record<string, string> = {\n \".liquid\": \"text/x-liquid\",\n \".json\": \"application/json\",\n \".css\": \"text/css\",\n \".js\": \"application/javascript\",\n \".html\": \"text/html\",\n \".txt\": \"text/plain\",\n \".md\": \"text/markdown\",\n \".svg\": \"image/svg+xml\",\n};\n\nconst BINARY_TYPES: Record<string, string> = {\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".eot\": \"application/vnd.ms-fontobject\",\n \".otf\": \"font/otf\",\n \".pdf\": \"application/pdf\",\n \".zip\": \"application/zip\",\n \".mp4\": \"video/mp4\",\n \".webm\": \"video/webm\",\n \".mp3\": \"audio/mpeg\",\n \".wav\": \"audio/wav\",\n};\n\nexport interface MimeType {\n name: string;\n isText: boolean;\n}\n\nexport function mimeTypeFor(ext: string): MimeType {\n const text = TEXT_TYPES[ext];\n if (text) return { name: text, isText: true };\n\n const binary = BINARY_TYPES[ext];\n if (binary) return { name: binary, isText: false };\n\n return { name: \"application/octet-stream\", isText: false };\n}\n","const THEME_LEVEL_RESOURCE_KEYS = new Set([\n \"global_styles.css\",\n \"styles.css\",\n \"variables.json\",\n]);\n\nconst COMPOSITE_RESOURCE_FILE_NAMES = new Set([\n \"index.liquid\",\n \"styles.css\",\n \"variables.json\",\n]);\n\nfunction hasSafeSegments(key: string): boolean {\n const segments = key.split(\"/\");\n return segments.every(\n (segment) => segment.length > 0 && segment !== \".\" && segment !== \"..\",\n );\n}\n\nexport function normalizeThemeResourceKey(value: string): string {\n return value.replaceAll(\"\\\\\", \"/\");\n}\n\n/**\n * Whether a relative file path is a resource key accepted by the Fluid themes\n * resource API. Local project files (package manifests, QA evidence, scripts,\n * source baselines, and similar agent artifacts) must never be uploaded.\n */\nexport function isThemeResourceKey(relativePath: string): boolean {\n const key = normalizeThemeResourceKey(relativePath);\n if (!hasSafeSegments(key)) return false;\n if (THEME_LEVEL_RESOURCE_KEYS.has(key)) return true;\n\n const segments = key.split(\"/\");\n const prefix = segments[0];\n const fileName = segments.at(-1);\n if (!prefix || !fileName || segments.length < 2) return false;\n\n if (\n prefix === \"assets\" ||\n prefix === \"config\" ||\n prefix === \"locales\" ||\n prefix === \"layouts\"\n ) {\n return true;\n }\n\n return segments.length >= 3 && COMPOSITE_RESOURCE_FILE_NAMES.has(fileName);\n}\n","","import settingTypesJson from \"./setting-types.json\" with { type: \"json\" };\n\nexport type SettingType =\n | \"text\"\n | \"plaintext\"\n | \"rich_text\"\n | \"richtext\"\n | \"textarea\"\n | \"html\"\n | \"html_textarea\"\n | \"url\"\n | \"range\"\n | \"number\"\n | \"select\"\n | \"radio\"\n | \"checkbox\"\n | \"color\"\n | \"color_background\"\n | \"font\"\n | \"font_picker\"\n | \"image\"\n | \"image_picker\"\n | \"video_picker\"\n | \"media_picker\"\n | \"text_alignment\"\n | \"media_fit\"\n | \"corner_radius\"\n | \"padding\"\n | \"border\"\n | \"gradient_overlay\"\n | \"header\"\n | \"product\"\n | \"products\"\n | \"collection\"\n | \"collections\"\n | \"category\"\n | \"categories\"\n | \"blog\"\n | \"posts\"\n | \"post\"\n | \"enrollment\"\n | \"enrollments\"\n | \"enrollment_pack\"\n | \"forms\"\n | \"media\"\n | \"variant\"\n | \"link_list\"\n | \"product_list\"\n | \"products_list\"\n | \"collection_list\"\n | \"collections_list\"\n | \"category_list\"\n | \"categories_list\"\n | \"posts_list\"\n | \"enrollment_list\"\n | \"enrollments_list\"\n | \"blog_list\"\n | \"blogs_list\"\n | \"post_list\"\n | \"enrollment_packs_list\";\n\n// Runtime list loaded from the canonical JSON — used for validation.\nexport const VALID_SETTING_TYPES: readonly string[] = Object.values(\n settingTypesJson.types as Record<string, string[]>,\n).flat();\n\n// Compile-time drift guard: if a type exists in the SettingType union but\n// not in setting-types.json, this object literal will error on the missing key.\n// When adding types to setting-types.json, also add them to SettingType above.\nconst _settingTypeCheck: Record<SettingType, true> = {\n text: true,\n plaintext: true,\n rich_text: true,\n richtext: true,\n textarea: true,\n html: true,\n html_textarea: true,\n url: true,\n range: true,\n number: true,\n select: true,\n radio: true,\n checkbox: true,\n color: true,\n color_background: true,\n font: true,\n font_picker: true,\n image: true,\n image_picker: true,\n video_picker: true,\n media_picker: true,\n text_alignment: true,\n media_fit: true,\n corner_radius: true,\n padding: true,\n border: true,\n gradient_overlay: true,\n header: true,\n product: true,\n products: true,\n collection: true,\n collections: true,\n category: true,\n categories: true,\n blog: true,\n posts: true,\n post: true,\n enrollment: true,\n enrollments: true,\n enrollment_pack: true,\n forms: true,\n media: true,\n variant: true,\n link_list: true,\n product_list: true,\n products_list: true,\n collection_list: true,\n collections_list: true,\n category_list: true,\n categories_list: true,\n posts_list: true,\n enrollment_list: true,\n enrollments_list: true,\n blog_list: true,\n blogs_list: true,\n post_list: true,\n enrollment_packs_list: true,\n} satisfies Record<SettingType, true>;\nvoid _settingTypeCheck;\n\nexport interface SelectOption {\n label: string;\n value: string;\n}\n\nexport interface SchemaSetting {\n type: SettingType;\n id: string;\n default?: string | number | boolean | null;\n label?: string;\n options?: SelectOption[];\n min?: number;\n max?: number;\n step?: number;\n unit?: string;\n content?: string;\n visible_if?: Record<string, unknown>;\n}\n\nexport interface SchemaBlock {\n type: string;\n name?: string;\n limit?: number;\n settings?: SchemaSetting[];\n blocks?: SchemaBlock[];\n}\n\nexport interface SchemaPreset {\n name?: string;\n category?: string;\n settings?: Record<string, unknown>;\n blocks?: Array<{ type: string; settings?: Record<string, unknown> }>;\n}\n\nexport interface SectionSchema {\n name?: string;\n tag?: string;\n class?: string;\n enabled_on?: { templates?: string[] };\n disabled_on?: { templates?: string[] };\n max_blocks?: number;\n settings?: SchemaSetting[];\n blocks?: SchemaBlock[] | Record<string, unknown>;\n presets?: SchemaPreset[];\n}\n\nexport type BlocksSchemaType = \"array\" | \"object\" | \"unknown\";\n\n/**\n * Structured locator describing the schema element a diagnostic refers to.\n *\n * The rule logic (this package) is intentionally position-agnostic so it can\n * be shared by the CLI (`fluid theme push`) and the CodeMirror-based theme /\n * visual editor alike. Position-aware consumers use `target` to map a\n * diagnostic back to a source range without re-deriving the validation rules.\n */\nexport type SettingDiagnosticTarget = {\n kind: \"setting\";\n /** Index of the setting within its `settings` array. */\n index: number;\n /** The setting's `id`, when present — used to locate the offending entry. */\n settingId?: string;\n /** The setting's `type`, when present — used to locate the offending entry. */\n settingType?: string;\n /** Which field the diagnostic concerns. */\n field: \"id\" | \"type\";\n};\n\nexport type BlockDiagnosticTarget = {\n kind: \"block\";\n /** Index of the block within its `blocks` array. */\n index: number;\n /** The block's `type`, when present — used to locate the offending entry. */\n blockType?: string;\n /** Which field the diagnostic concerns. */\n field: \"type\" | \"name\" | \"settings\";\n};\n\nexport type SectionDiagnosticTarget = {\n kind: \"section\";\n /** The referenced section `type` that has no matching section file. */\n sectionType: string;\n /** The `{% section %}` tag's instance id, when the tag included one. */\n tagId?: string;\n};\n\nexport type DiagnosticTarget =\n | SettingDiagnosticTarget\n | BlockDiagnosticTarget\n | SectionDiagnosticTarget;\n\nexport interface Diagnostic {\n severity: \"error\" | \"warning\";\n message: string;\n /**\n * Optional structured locator so position-aware consumers (e.g. the\n * CodeMirror-based theme editor) can map a diagnostic back to a source\n * range. The CLI ignores this and only renders `message`.\n */\n target?: DiagnosticTarget;\n}\n","import { VALID_SETTING_TYPES } from \"./types\";\nimport type { Diagnostic } from \"./types\";\n\n/**\n * Message shown when a setting declares a `type` that is not one of the\n * canonical `VALID_SETTING_TYPES`. Centralized here so the CLI and the editor\n * render identical text. Kept to a single line — the list of valid types is a\n * static set exposed once via the `VALID_SETTING_TYPES` export, so repeating it\n * in every diagnostic only bloats structured output.\n */\nexport function invalidSettingTypeMessage(type: string): string {\n return `Invalid settings type: '${type}'`;\n}\n\nexport function validateSettings(settings: unknown[]): Diagnostic[] {\n const diagnostics: Diagnostic[] = [];\n const ids = new Set<string>();\n\n for (let index = 0; index < settings.length; index++) {\n const raw = settings[index];\n const setting: Record<string, unknown> =\n raw !== null && typeof raw === \"object\" && !Array.isArray(raw)\n ? (raw as Record<string, unknown>)\n : {};\n\n const id = typeof setting.id === \"string\" ? setting.id : undefined;\n const type = typeof setting.type === \"string\" ? setting.type : undefined;\n\n if (id !== undefined && id.trim() === \"\") {\n diagnostics.push({\n severity: \"error\",\n message: \"Error in settings: id cannot be empty\",\n target: { kind: \"setting\", index, settingType: type, field: \"id\" },\n });\n } else if (id && ids.has(id)) {\n diagnostics.push({\n severity: \"error\",\n message: `Error in settings: duplicate id '${id}' found`,\n target: { kind: \"setting\", index, settingId: id, field: \"id\" },\n });\n } else if (id) {\n ids.add(id);\n }\n\n if (!type) {\n diagnostics.push({\n severity: \"error\",\n message: `Error in setting '${id ?? index}': missing required field 'type'`,\n target: { kind: \"setting\", index, settingId: id, field: \"type\" },\n });\n } else if (!VALID_SETTING_TYPES.includes(type)) {\n diagnostics.push({\n severity: \"error\",\n message: invalidSettingTypeMessage(type),\n target: { kind: \"setting\", index, settingType: type, field: \"type\" },\n });\n }\n }\n\n return diagnostics;\n}\n","import type { Diagnostic } from \"./types\";\nimport { validateSettings } from \"./validate-settings\";\n\nexport function validateBlocks(blocks: unknown[]): Diagnostic[] {\n const diagnostics: Diagnostic[] = [];\n const types = new Set<string>();\n\n for (let index = 0; index < blocks.length; index++) {\n const raw = blocks[index];\n const block: Record<string, unknown> =\n raw !== null && typeof raw === \"object\" && !Array.isArray(raw)\n ? (raw as Record<string, unknown>)\n : {};\n\n const type = typeof block.type === \"string\" ? block.type : undefined;\n const name = typeof block.name === \"string\" ? block.name : undefined;\n const settings = block.settings;\n\n if (!type) {\n diagnostics.push({\n severity: \"error\",\n message: `Error in blocks at index ${index}: missing required field 'type'`,\n target: { kind: \"block\", index, field: \"type\" },\n });\n } else if (types.has(type)) {\n diagnostics.push({\n severity: \"warning\",\n message: `Warning in blocks: duplicate type '${type}' found`,\n target: { kind: \"block\", index, blockType: type, field: \"type\" },\n });\n } else {\n types.add(type);\n }\n\n // Named block references (type only, no name or settings) point to\n // standalone block templates — skip the name requirement for those.\n const isNamedBlockRef = !name && !settings;\n if (!name && type !== \"@app\" && type !== \"@theme\" && !isNamedBlockRef) {\n diagnostics.push({\n severity: \"error\",\n message: `Error in block '${type ?? index}': missing required field 'name'`,\n target: { kind: \"block\", index, blockType: type, field: \"name\" },\n });\n }\n\n if (settings) {\n if (!Array.isArray(settings)) {\n // e.g. the author wrote `\"settings\": {}` instead of `\"settings\": []`.\n diagnostics.push({\n severity: \"error\",\n message: `Error in block '${type ?? index}': 'settings' must be an array ([])`,\n target: { kind: \"block\", index, blockType: type, field: \"settings\" },\n });\n } else {\n diagnostics.push(...validateSettings(settings));\n }\n }\n\n // Recurse into nested blocks (max 2 levels enforced by the engine,\n // but we validate whatever is declared)\n if (Array.isArray(block.blocks)) {\n diagnostics.push(...validateBlocks(block.blocks as unknown[]));\n }\n }\n\n return diagnostics;\n}\n","import type { BlocksSchemaType, Diagnostic } from \"./types\";\nimport { validateSettings } from \"./validate-settings\";\nimport { validateBlocks } from \"./validate-blocks\";\n\n// Strip Liquid comment blocks so they don't interfere with schema extraction.\nfunction stripLiquidComments(text: string): string {\n return text.replace(\n /\\{%-?\\s*comment\\s*-?%\\}[\\s\\S]*?\\{%-?\\s*endcomment\\s*-?%\\}/g,\n \"\",\n );\n}\n\n// Detect duplicate \"blocks\" keys in the same JSON object.\n// Standard JSON.parse silently drops duplicates, so we scan tokens manually.\nfunction findDuplicateBlocksKeys(jsonText: string): number {\n let count = 0;\n const stack: Array<{\n type: \"object\" | \"array\";\n keys: Set<string>;\n expectingKey: boolean;\n }> = [];\n let pendingKey: string | null = null;\n let i = 0;\n\n while (i < jsonText.length) {\n const ch = jsonText.charCodeAt(i);\n\n // Whitespace\n if (ch === 0x20 || ch === 0x0a || ch === 0x0d || ch === 0x09) {\n i++;\n continue;\n }\n\n if (ch === 0x7b) {\n // {\n stack.push({ type: \"object\", keys: new Set(), expectingKey: true });\n pendingKey = null;\n i++;\n } else if (ch === 0x7d) {\n // }\n stack.pop();\n pendingKey = null;\n i++;\n } else if (ch === 0x5b) {\n // [\n pendingKey = null;\n stack.push({ type: \"array\", keys: new Set(), expectingKey: false });\n i++;\n } else if (ch === 0x5d) {\n // ]\n stack.pop();\n pendingKey = null;\n i++;\n } else if (ch === 0x3a) {\n // :\n i++;\n } else if (ch === 0x2c) {\n // ,\n const top = stack[stack.length - 1];\n if (top?.type === \"object\") {\n top.expectingKey = true;\n }\n pendingKey = null;\n i++;\n } else if (ch === 0x22) {\n // \"\n let j = i + 1;\n while (j < jsonText.length) {\n if (\n jsonText.charCodeAt(j) === 0x22 &&\n jsonText.charCodeAt(j - 1) !== 0x5c\n ) {\n break;\n }\n j++;\n }\n const str = jsonText.slice(i + 1, j);\n i = j + 1;\n\n const top = stack[stack.length - 1];\n if (top?.type === \"object\" && top.expectingKey) {\n if (str === \"blocks\" && top.keys.has(str)) {\n count++;\n }\n top.keys.add(str);\n top.expectingKey = false;\n pendingKey = str;\n } else {\n pendingKey = null;\n }\n } else {\n pendingKey = null;\n i++;\n }\n }\n\n return count;\n}\n\nexport interface ValidateSchemaOptions {\n blocksSchemaType?: BlocksSchemaType;\n}\n\n// Validate the full Liquid file content containing a {% schema %} block.\n// Returns an array of diagnostics (empty = valid).\nexport function validateSchemaText(\n text: string,\n options?: ValidateSchemaOptions,\n): Diagnostic[] {\n const blocksSchemaType = options?.blocksSchemaType ?? \"unknown\";\n const diagnostics: Diagnostic[] = [];\n\n const stripped = stripLiquidComments(text);\n const match = stripped.match(\n /\\{%-?\\s*schema\\s*-?%\\}([\\s\\S]*?)\\{%-?\\s*endschema\\s*-?%\\}/,\n );\n if (!match) return diagnostics;\n\n const jsonText = match[1] ?? \"\";\n\n let schema: Record<string, unknown>;\n try {\n schema = JSON.parse(jsonText) as Record<string, unknown>;\n } catch (e) {\n diagnostics.push({\n severity: \"error\",\n message: `Invalid JSON:\\n ${(e as Error).message}`,\n });\n return diagnostics;\n }\n\n // Duplicate \"blocks\" keys\n const dupes = findDuplicateBlocksKeys(jsonText);\n for (let d = 0; d < dupes; d++) {\n diagnostics.push({\n severity: \"error\",\n message: \"Error in blocks: duplicate 'blocks' key in the same object\",\n });\n }\n\n // Settings\n if (\n schema !== null &&\n typeof schema === \"object\" &&\n Array.isArray(schema.settings)\n ) {\n diagnostics.push(...validateSettings(schema.settings));\n }\n\n // Blocks\n if (schema !== null && typeof schema === \"object\" && \"blocks\" in schema) {\n const blocks = schema.blocks;\n\n if (blocksSchemaType === \"array\") {\n if (!Array.isArray(blocks)) {\n diagnostics.push({\n severity: \"error\",\n message: \"Error in blocks: expected an array ([])\",\n });\n } else {\n diagnostics.push(...validateBlocks(blocks));\n }\n } else if (blocksSchemaType === \"object\") {\n if (\n Array.isArray(blocks) ||\n typeof blocks !== \"object\" ||\n blocks === null\n ) {\n diagnostics.push({\n severity: \"error\",\n message: \"Error in blocks: expected an object ({})\",\n });\n }\n } else {\n // \"unknown\" — validate if array, accept if object\n if (Array.isArray(blocks)) {\n diagnostics.push(...validateBlocks(blocks));\n }\n }\n }\n\n return diagnostics;\n}\n\n// Validate a parsed schema object directly (when you already have the JSON).\nexport function validateSchema(\n schema: Record<string, unknown>,\n options?: ValidateSchemaOptions,\n): Diagnostic[] {\n const blocksSchemaType = options?.blocksSchemaType ?? \"unknown\";\n const diagnostics: Diagnostic[] = [];\n\n if (Array.isArray(schema.settings)) {\n diagnostics.push(...validateSettings(schema.settings));\n }\n\n if (\"blocks\" in schema) {\n const blocks = schema.blocks;\n\n if (blocksSchemaType === \"array\") {\n if (!Array.isArray(blocks)) {\n diagnostics.push({\n severity: \"error\",\n message: \"Error in blocks: expected an array ([])\",\n });\n } else {\n diagnostics.push(...validateBlocks(blocks));\n }\n } else if (blocksSchemaType === \"object\") {\n if (\n Array.isArray(blocks) ||\n typeof blocks !== \"object\" ||\n blocks === null\n ) {\n diagnostics.push({\n severity: \"error\",\n message: \"Error in blocks: expected an object ({})\",\n });\n }\n } else {\n if (Array.isArray(blocks)) {\n diagnostics.push(...validateBlocks(blocks));\n }\n }\n }\n\n return diagnostics;\n}\n","import type { Diagnostic } from \"./types\";\n\n// Liquid comment blocks — stripped so commented-out section tags are not\n// treated as real references.\nconst LIQUID_COMMENT_REGEX =\n /\\{%-?\\s*comment\\s*-?%\\}[\\s\\S]*?\\{%-?\\s*endcomment\\s*-?%\\}/g;\n\n// The `{% schema %} … {% endschema %}` block — removed so section types\n// declared inside the schema JSON are not mistaken for `{% section %}` usages.\nconst SCHEMA_BLOCK_REGEX =\n /\\{%-?\\s*schema\\s*-?%\\}[\\s\\S]*?\\{%-?\\s*endschema\\s*-?%\\}/;\n\n// Matches a `{% section %}` tag. Supports both the id-bearing form\n// (`{% section 'hero', id: 'abc' %}`) emitted by the visual editor and the\n// bare form (`{% section 'hero' %}`) hand-authored themes use, plus\n// whitespace-control tags (`{%- … -%}`) and single or double quotes.\n// `id` is optional — capture group 2 is undefined for bare tags.\nconst SECTION_TAG_PATTERN =\n \"\\\\{%-?\\\\s*section\\\\s+['\\\"]([^'\\\"]+)['\\\"](?:\\\\s*,\\\\s*id:\\\\s*['\\\"]([^'\\\"]+)['\\\"])?\\\\s*-?%\\\\}\";\n\n// Reserved layout-region section types. `{% section 'navbar' %}` and friends\n// resolve to the theme's navbar/footer/library_navbar template slots rather than\n// a `sections/<name>` definition, and render empty when absent — so they are\n// never \"missing\". Mirrors `LiquidTags::Section::SECTION_TEMPLATES` server-side.\nconst RESERVED_SECTION_TYPES = new Set([\"navbar\", \"library_navbar\", \"footer\"]);\n\n// `fluid://extensions/{id}/{type}/{name}` references resolve against an app\n// extension installed on the company at render time — they cannot be validated\n// against local files, so they are never flagged. Mirrors `Themes::ExtensionUri`.\nconst EXTENSION_URI_PATTERN = /^fluid:\\/\\/extensions\\/[^/]+\\/[^/]+\\/[^/]+$/;\n\n/** Whether a `{% section %}` type resolves to something other than an on-disk `sections/<name>` definition (and so cannot be flagged as missing). */\nexport function isNonLocalSectionType(type: string): boolean {\n return RESERVED_SECTION_TYPES.has(type) || EXTENSION_URI_PATTERN.test(type);\n}\n\nexport interface SectionReference {\n /** The referenced section type/name (group 1). */\n type: string;\n /** The section instance id, when the tag declares one. */\n id?: string;\n /** The full matched tag text. */\n fullTag: string;\n /** 0-based position of the tag within the template body. */\n order: number;\n}\n\n/** A template, identified by `path`, with its raw liquid `content`. */\nexport interface TemplateInput {\n path: string;\n content: string;\n}\n\n// Liquid outside the comment and schema blocks — the rendered template body\n// where `{% section %}` references actually live.\nfunction templateBody(liquid: string): string {\n return liquid\n .replace(LIQUID_COMMENT_REGEX, \"\")\n .replace(SCHEMA_BLOCK_REGEX, \"\");\n}\n\n/**\n * Extract every `{% section %}` reference from a liquid template, ignoring\n * tags inside comments or the `{% schema %}` block. Shared by the editor's\n * section-usage detection and the CLI linter so both parse references\n * identically.\n */\nexport function extractSectionReferences(liquid: string): SectionReference[] {\n const body = templateBody(liquid);\n const pattern = new RegExp(SECTION_TAG_PATTERN, \"g\");\n const references: SectionReference[] = [];\n let order = 0;\n let match: RegExpExecArray | null;\n while ((match = pattern.exec(body)) !== null) {\n const type = match[1];\n if (!type) continue;\n references.push({ type, id: match[2], fullTag: match[0], order: order++ });\n }\n return references;\n}\n\n/**\n * Paths of the templates that reference `sectionName`. Used by the editor to\n * warn before deleting a section that is still in use.\n */\nexport function findTemplatesReferencingSection(\n templates: TemplateInput[],\n sectionName: string,\n): string[] {\n const matches: string[] = [];\n for (const template of templates) {\n const references = extractSectionReferences(template.content);\n if (references.some((reference) => reference.type === sectionName)) {\n matches.push(template.path);\n }\n }\n return matches;\n}\n\nexport interface MissingSectionRef {\n templatePath: string;\n sectionType: string;\n diagnostic: Diagnostic;\n}\n\n/**\n * Find `{% section %}` references that point to a section that does not exist\n * in `existingSectionNames` — the static equivalent of \"an in-use section was\n * deleted\". Reserved layout-region types (navbar/footer/library_navbar) and\n * `fluid://` extension URIs are never flagged (see `isNonLocalSectionType`).\n * Emits one `error` diagnostic per missing section type per template.\n */\nexport function findMissingSectionReferences(\n templates: TemplateInput[],\n existingSectionNames: Set<string>,\n): MissingSectionRef[] {\n const missing: MissingSectionRef[] = [];\n for (const template of templates) {\n const reported = new Set<string>();\n for (const reference of extractSectionReferences(template.content)) {\n if (existingSectionNames.has(reference.type)) continue;\n if (isNonLocalSectionType(reference.type)) continue;\n if (reported.has(reference.type)) continue;\n reported.add(reference.type);\n missing.push({\n templatePath: template.path,\n sectionType: reference.type,\n diagnostic: {\n severity: \"error\",\n message: `references missing section '${reference.type}'`,\n target: {\n kind: \"section\",\n sectionType: reference.type,\n tagId: reference.id,\n },\n },\n });\n }\n }\n return missing;\n}\n","import {\n readFileSync,\n writeFileSync,\n mkdirSync,\n existsSync,\n statSync,\n} from \"node:fs\";\nimport { extname, basename, relative, dirname } from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { mimeTypeFor, type MimeType } from \"./mime-type.js\";\nimport { normalizeThemeResourceKey } from \"./resource-key.js\";\nimport {\n validateSchemaText,\n type Diagnostic,\n type BlocksSchemaType,\n} from \"@fluid-app/theme-schema\";\n\n// Top-level theme folders that are not page templates. Everything else at the\n// top level (home_page, product, page, footer, navbar, …) is a page template.\nconst NON_TEMPLATE_DIRS = new Set([\n \"sections\",\n \"blocks\",\n \"components\",\n \"layouts\",\n \"config\",\n \"assets\",\n \"locales\",\n]);\n\nexport class ThemeFile {\n readonly absolutePath: string;\n readonly relativePath: string;\n readonly mime: MimeType;\n\n constructor(absolutePath: string, root: string) {\n this.absolutePath = absolutePath;\n this.relativePath = normalizeThemeResourceKey(relative(root, absolutePath));\n this.mime = mimeTypeFor(extname(absolutePath).toLowerCase());\n }\n\n get name(): string {\n return basename(this.absolutePath);\n }\n\n get isText(): boolean {\n return this.mime.isText;\n }\n\n get isLiquid(): boolean {\n return this.absolutePath.endsWith(\".liquid\");\n }\n\n get isJson(): boolean {\n return this.absolutePath.endsWith(\".json\");\n }\n\n get exists(): boolean {\n return existsSync(this.absolutePath);\n }\n\n read(): string {\n return readFileSync(this.absolutePath, \"utf-8\");\n }\n\n readBinary(): Buffer {\n return readFileSync(this.absolutePath);\n }\n\n write(content: string | Buffer): void {\n mkdirSync(dirname(this.absolutePath), { recursive: true });\n if (typeof content === \"string\") {\n writeFileSync(this.absolutePath, content, \"utf-8\");\n } else {\n writeFileSync(this.absolutePath, content);\n }\n }\n\n checksum(): string {\n const content = this.isText ? this.read() : this.readBinary();\n return createHash(\"sha256\").update(content).digest(\"hex\");\n }\n\n size(): number {\n return statSync(this.absolutePath).size;\n }\n\n get isTemplate(): boolean {\n // Page templates (home_page, product, footer, navbar, …) live in top-level\n // page-type folders and expect blocks as objects. The reserved categories\n // below either expect blocks as arrays (sections, blocks, components) or\n // carry no block schema (layouts, config, assets, locales).\n const parts = this.relativePath.split(/[/\\\\]/);\n return parts.length >= 2 && !NON_TEMPLATE_DIRS.has(parts[0]!);\n }\n\n validateSchema(): Diagnostic[] {\n if (!this.isLiquid) return [];\n\n const blocksSchemaType: BlocksSchemaType = this.isTemplate\n ? \"object\"\n : \"array\";\n\n return validateSchemaText(this.read(), { blocksSchemaType });\n }\n}\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\n\nconst IGNORE_FILE = \".fluidignore\";\n\ninterface Pattern {\n negated: boolean;\n pattern: string;\n}\n\nexport class FluidIgnore {\n private patterns: Pattern[];\n\n constructor(root: string) {\n this.patterns = this.parse(join(root, IGNORE_FILE));\n }\n\n ignore(relativePath: string): boolean {\n let result = false;\n for (const { negated, pattern } of this.patterns) {\n if (this.match(pattern, relativePath)) {\n result = !negated;\n }\n }\n return result;\n }\n\n private parse(filePath: string): Pattern[] {\n if (!existsSync(filePath)) return [];\n return readFileSync(filePath, \"utf-8\")\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l && !l.startsWith(\"#\"))\n .map((l) => {\n const negated = l.startsWith(\"!\");\n let pattern = negated ? l.slice(1) : l;\n if (pattern.startsWith(\"/\")) pattern = pattern.slice(1);\n return { negated, pattern };\n });\n }\n\n private match(pattern: string, path: string): boolean {\n if (pattern.endsWith(\"/\")) {\n return path.startsWith(pattern) || path === pattern.slice(0, -1);\n }\n if (pattern.includes(\"/\")) {\n return this.fnmatch(pattern, path);\n }\n return this.fnmatch(pattern, path) || this.fnmatch(pattern, basename(path));\n }\n\n private fnmatch(pattern: string, str: string): boolean {\n const re = pattern\n .split(\"**\")\n .map((p) =>\n p\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/\\?/g, \"[^/]\"),\n )\n .join(\".*\");\n return new RegExp(`^${re}$`).test(str);\n }\n}\n","import { existsSync, readdirSync, statSync } from \"node:fs\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport { ThemeFile } from \"./file.js\";\nimport { FluidIgnore } from \"./fluid-ignore.js\";\nimport { isThemeResourceKey } from \"./resource-key.js\";\n\nconst THEME_MARKERS = [\"templates\", \"assets\", \"config\"];\nconst THEME_ASSET_MANIFEST = \".fluid-assets.json\";\n\nexport class ThemeRoot {\n readonly root: string;\n readonly ignore: FluidIgnore;\n\n constructor(root: string) {\n this.root = resolve(root);\n this.ignore = new FluidIgnore(this.root);\n }\n\n isValid(): boolean {\n return (\n existsSync(join(this.root, THEME_ASSET_MANIFEST)) ||\n THEME_MARKERS.some((m) => {\n try {\n return statSync(join(this.root, m)).isDirectory();\n } catch {\n return false;\n }\n })\n );\n }\n\n files(): ThemeFile[] {\n return this.glob(this.root).filter(\n (f) =>\n isThemeResourceKey(f.relativePath) &&\n !this.ignore.ignore(f.relativePath),\n );\n }\n\n isResourcePath(pathOrFile: string | ThemeFile): boolean {\n const file = this.file(pathOrFile);\n return isThemeResourceKey(file.relativePath);\n }\n\n file(pathOrFile: string | ThemeFile): ThemeFile {\n if (pathOrFile instanceof ThemeFile) return pathOrFile;\n const abs = isAbsolute(pathOrFile)\n ? pathOrFile\n : join(this.root, pathOrFile);\n return new ThemeFile(abs, this.root);\n }\n\n private glob(dir: string): ThemeFile[] {\n const results: ThemeFile[] = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name.startsWith(\".\")) continue;\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n if (entry.name === \"node_modules\") continue;\n results.push(...this.glob(full));\n } else if (entry.isFile()) {\n results.push(new ThemeFile(full, this.root));\n }\n }\n return results;\n }\n}\n","import type { ServerResponse } from \"node:http\";\n\nexport class SSEStream {\n private responses = new Set<ServerResponse>();\n\n add(res: ServerResponse): void {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n \"Access-Control-Allow-Origin\": \"*\",\n });\n res.write(\":\\n\\n\");\n this.responses.add(res);\n res.on(\"close\", () => this.responses.delete(res));\n }\n\n broadcast(data: string): void {\n const payload = `data: ${data}\\n\\n`;\n for (const res of this.responses) {\n try {\n res.write(payload);\n } catch {\n this.responses.delete(res);\n }\n }\n }\n\n close(): void {\n for (const res of this.responses) {\n try {\n res.end();\n } catch {\n // ignore\n }\n }\n this.responses.clear();\n }\n\n get size(): number {\n return this.responses.size;\n }\n}\n","export function buildHotReloadScript(mode: \"full-page\" | \"off\"): string {\n return `\n<script>\n(() => {\n window.__FLUID_CLI_ENV__ = ${JSON.stringify({ mode })};\n\n class HotReload {\n static reloadMode() { return window.__FLUID_CLI_ENV__.mode; }\n static isActive() { return HotReload.reloadMode() !== \"off\"; }\n static setHotReloadCookie(files) {\n const expires = new Date(Date.now() + 3000).toUTCString();\n document.cookie = \\`hot_reload_files=\\${files.join(\",\")};expires=\\${expires};path=/\\`;\n }\n static refresh(files) {\n HotReload.setHotReloadCookie(files);\n console.log(\"[HotReload] Refreshing page\");\n window.location.reload();\n }\n }\n\n class SSEClient {\n constructor(url, handler) {\n if (typeof EventSource === \"undefined\") {\n console.error(\"[HotReload] EventSource not supported in this browser.\");\n return;\n }\n console.log(\"[HotReload] Initializing…\");\n this.url = url;\n this.handler = handler;\n }\n connect() {\n const es = new EventSource(this.url);\n es.onopen = () => console.log(\"[HotReload] SSE connected.\");\n es.onerror = () => {\n console.log(\"[HotReload] SSE closed. Reconnecting in 5s…\");\n es.close();\n setTimeout(() => this.connect(), 5000);\n };\n es.onmessage = (msg) => {\n const data = JSON.parse(msg.data);\n if (data.reload_page) { HotReload.refresh([]); return; }\n this.handler(data);\n };\n }\n }\n\n if (HotReload.isActive()) {\n new SSEClient(\"/hot-reload\", (data) => {\n if (data.modified) HotReload.refresh(data.modified);\n }).connect();\n }\n})();\n</script>`;\n}\n\nexport function injectHotReload(\n html: string,\n mode: \"full-page\" | \"off\",\n): string {\n const script = buildHotReloadScript(mode);\n if (html.includes(\"</body>\")) {\n return html.replace(\"</body>\", `${script}\\n</body>`);\n }\n return html + script;\n}\n","import https from \"node:https\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { injectHotReload } from \"./hot-reload.js\";\nimport { getAuthToken } from \"@fluid-app/fluid-cli\";\n\nconst HOP_BY_HOP = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n \"content-security-policy\",\n]);\n\nexport interface ProxyOptions {\n company: string;\n themeId: number;\n reloadMode: \"full-page\" | \"off\";\n pendingFiles?: () => Array<{ relativePath: string; read: () => string }>;\n}\n\nexport async function proxyRequest(\n req: IncomingMessage,\n res: ServerResponse,\n opts: ProxyOptions,\n): Promise<void> {\n const companyHost = `${opts.company}.fluid.app`;\n\n const headers: Record<string, string> = {};\n for (const [k, v] of Object.entries(req.headers)) {\n if (!HOP_BY_HOP.has(k.toLowerCase()) && typeof v === \"string\") {\n headers[k] = v;\n }\n }\n headers[\"host\"] = companyHost;\n headers[\"x-fluid-theme\"] = String(opts.themeId);\n headers[\"user-agent\"] = \"Fluid CLI\";\n headers[\"accept-encoding\"] = \"identity\";\n\n const url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n url.searchParams.set(\"_fd\", \"0\");\n url.searchParams.set(\"pb\", \"0\");\n\n const pending = opts.pendingFiles?.() ?? [];\n const isGet = req.method === \"GET\" || req.method === \"HEAD\";\n let method = req.method ?? \"GET\";\n let body: string | Buffer | undefined;\n\n if (pending.length > 0 && isGet) {\n method = \"POST\";\n const params = new URLSearchParams();\n params.set(\"_method\", req.method ?? \"GET\");\n for (const f of pending) {\n params.set(`replace_templates[${f.relativePath}]`, f.read());\n }\n const token = getAuthToken();\n if (token) headers[\"authorization\"] = `Bearer ${token}`;\n headers[\"content-type\"] = \"application/x-www-form-urlencoded\";\n body = params.toString();\n headers[\"content-length\"] = String(Buffer.byteLength(body));\n } else if (!isGet) {\n body = await readBody(req);\n if (body.length > 0) {\n headers[\"content-length\"] = String(body.length);\n }\n }\n\n return new Promise((resolve, reject) => {\n const options: https.RequestOptions = {\n hostname: companyHost,\n port: 443,\n path: url.pathname + (url.search || \"\"),\n method,\n headers,\n };\n\n const proxyReq = https.request(options, (proxyRes) => {\n const contentType = proxyRes.headers[\"content-type\"] ?? \"\";\n const isHtml = contentType.includes(\"text/html\");\n\n const responseHeaders: Record<string, string | string[]> = {};\n for (const [k, v] of Object.entries(proxyRes.headers)) {\n if (!HOP_BY_HOP.has(k.toLowerCase()) && v !== undefined) {\n responseHeaders[k] = v as string | string[];\n }\n }\n\n if (isHtml) {\n const chunks: Buffer[] = [];\n proxyRes.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n proxyRes.on(\"end\", () => {\n let html = Buffer.concat(chunks).toString(\"utf-8\");\n html = injectHotReload(html, opts.reloadMode);\n responseHeaders[\"content-length\"] = String(Buffer.byteLength(html));\n res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);\n res.end(html);\n resolve();\n });\n } else {\n res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);\n proxyRes.pipe(res);\n proxyRes.on(\"end\", resolve);\n }\n });\n\n proxyReq.on(\"error\", (err) => {\n reject(err);\n });\n\n if (body) proxyReq.write(body);\n proxyReq.end();\n });\n}\n\nfunction readBody(req: IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n","/**\n * Generated API client functions for v0\n *\n * DO NOT EDIT THIS FILE DIRECTLY\n * This file is auto-generated. To update:\n * 1. Update the OpenAPI spec file\n * 2. Run: pnpm generate\n */\n\nimport type { FetchClient } from \"../lib/fetch-client\";\nimport type { operations } from \"../generated/v0\";\n\n// ============================================================================\n// applicationthemetemplates\n// ============================================================================\n\n/**\n * Lists all theme templates\n * \n *\n * @param client - Fetch client instance\n \n */\nexport async function listThemeTemplates(\n client: FetchClient,\n): Promise<\n operations[\"listThemeTemplates\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_theme_templates`);\n}\n\n/**\n * Creates a theme template\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createThemeTemplate(\n client: FetchClient,\n body: NonNullable<\n operations[\"createThemeTemplate\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createThemeTemplate\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates`, body);\n}\n\n/**\n * List all mysite themes\n * List all mysite themes\n *\n * @param client - Fetch client instance\n \n */\nexport async function listMysiteThemes(\n client: FetchClient,\n): Promise<\n operations[\"listMysiteThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_theme_templates/mysite_themes`);\n}\n\n/**\n * Retrieves a theme template\n * Returns a theme template with details. For section templates whose schema\ndeclares `@theme` or named standalone block references, the response\nincludes an `available_theme_blocks` array with the resolved block schemas\n(name, settings, presets). Private blocks (underscore-prefixed) are excluded\nfrom `@theme` results but included when explicitly referenced by name.\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_theme_templates/${id}`);\n}\n\n/**\n * Updates a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateThemeTemplate(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateThemeTemplate\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/application_theme_templates/${id}`, body);\n}\n\n/**\n * Deletes a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"deleteThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/application_theme_templates/${id}`);\n}\n\n/**\n * Returns all available themeables for theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeTemplateAvailableThemeables(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeTemplateAvailableThemeables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_theme_templates/${id}/available_themeables`,\n );\n}\n\n/**\n * Get available variables for a theme template\n * Get available variables that can be used in the theme template\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeTemplateAvailableVariables(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeTemplateAvailableVariables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_theme_templates/${id}/available_variables`,\n );\n}\n\n/**\n * Clones a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function cloneThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"cloneThemeTemplate\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/clone`);\n}\n\n/**\n * Publishes the template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function publishThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"publishThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/publish`);\n}\n\n/**\n * Renders a page for a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function renderThemeTemplatePage(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"renderThemeTemplatePage\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"renderThemeTemplatePage\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(\n `/api/application_theme_templates/${id}/render_page`,\n body,\n );\n}\n\n/**\n * Renders a section template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function renderThemeTemplateSection(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"renderThemeTemplateSection\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/render_section`);\n}\n\n/**\n * Sets a theme template as default\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function setDefaultThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"setDefaultThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/set_default`);\n}\n\n/**\n * Updates themeable records to be used by the specified template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function updateThemeTemplateThemeables(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"updateThemeTemplateThemeables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.put(`/api/application_theme_templates/${id}/themeables_update`);\n}\n\n// ============================================================================\n// application-themes\n// ============================================================================\n\n/**\n * List application themes\n * Get all application themes with optional filters\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listApplicationThemes(\n client: FetchClient,\n params?: operations[\"listApplicationThemes\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listApplicationThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes`, params);\n}\n\n/**\n * Create an application theme\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createApplicationTheme(\n client: FetchClient,\n body: NonNullable<\n operations[\"createApplicationTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createApplicationTheme\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes`, body);\n}\n\n/**\n * Get current active application theme\n * \n *\n * @param client - Fetch client instance\n \n */\nexport async function getActiveApplicationTheme(\n client: FetchClient,\n): Promise<\n operations[\"getActiveApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes/active`);\n}\n\n/**\n * Import an application theme from zip file\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function importApplicationThemeFromZip(\n client: FetchClient,\n body: NonNullable<\n operations[\"importApplicationThemeFromZip\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"importApplicationThemeFromZip\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/import_zip`, body);\n}\n\n/**\n * Get an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param [params] - params\n */\nexport async function getApplicationTheme(\n client: FetchClient,\n id: string | number,\n params?: operations[\"getApplicationTheme\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"getApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes/${id}`, params);\n}\n\n/**\n * Update an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateApplicationTheme(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateApplicationTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/application_themes/${id}`, body);\n}\n\n/**\n * Delete an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"deleteApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/application_themes/${id}`);\n}\n\n/**\n * Returns available themeables for a given type scoped to the theme's company\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param [params] - params\n */\nexport async function getApplicationThemeAvailableThemeables(\n client: FetchClient,\n id: string | number,\n params?: operations[\"getApplicationThemeAvailableThemeables\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"getApplicationThemeAvailableThemeables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_themes/${id}/available_themeables`,\n params,\n );\n}\n\n/**\n * Clone an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function cloneApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"cloneApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/${id}/clone`);\n}\n\n/**\n * Create a development reference clone of an application theme\n * Creates an isolated development theme while preserving existing DAM and ImageKit references without transferring asset bytes.\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function cloneApplicationThemeForDevelopment(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"cloneApplicationThemeForDevelopment\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"cloneApplicationThemeForDevelopment\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(\n `/api/application_themes/${id}/clone_for_development`,\n body,\n );\n}\n\n/**\n * Import an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function importApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"importApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/${id}/import`);\n}\n\n/**\n * Publishes the theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function publishApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"publishApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/${id}/publish`);\n}\n\n/**\n * Get theme assets\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeAssets(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeAssets\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes/${id}/theme_assets`);\n}\n\n// ============================================================================\n// applicationthemeresources\n// ============================================================================\n\n/**\n * Lists all theme resources\n *\n *\n * @param client - Fetch client instance\n * @param application_theme_id - application_theme_id\n */\nexport async function listThemeResources(\n client: FetchClient,\n application_theme_id: string | number,\n): Promise<\n operations[\"listThemeResources\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_themes/${application_theme_id}/resources`,\n );\n}\n\n/**\n * Updates a theme resource\n *\n *\n * @param client - Fetch client instance\n * @param application_theme_id - application_theme_id\n * @param body - body\n */\nexport async function updateThemeResource(\n client: FetchClient,\n application_theme_id: string | number,\n body: NonNullable<\n operations[\"updateThemeResource\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateThemeResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.put(\n `/api/application_themes/${application_theme_id}/resources`,\n body,\n );\n}\n\n/**\n * Deletes a theme resource\n *\n *\n * @param client - Fetch client instance\n * @param application_theme_id - application_theme_id\n * @param body - body\n */\nexport async function deleteThemeResource(\n client: FetchClient,\n application_theme_id: string | number,\n body: NonNullable<\n operations[\"deleteThemeResource\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"deleteThemeResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(\n `/api/application_themes/${application_theme_id}/resources`,\n { body },\n );\n}\n\n// ============================================================================\n// file-resources\n// ============================================================================\n\n/**\n * Returns a list of file resources\n *\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listFileResources(\n client: FetchClient,\n params?: operations[\"listFileResources\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listFileResources\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/file_resources`, params);\n}\n\n/**\n * Creates a file resource\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createFileResource(\n client: FetchClient,\n body: NonNullable<\n operations[\"createFileResource\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createFileResource\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/file_resources`, body);\n}\n\n/**\n * Creates multiple file resources\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function bulkCreateFileResources(\n client: FetchClient,\n body: NonNullable<\n operations[\"bulkCreateFileResources\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"bulkCreateFileResources\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/file_resources/bulk_create`, body);\n}\n\n/**\n * Deletes multiple file resources\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function bulkDestroyFileResources(\n client: FetchClient,\n body: NonNullable<\n operations[\"bulkDestroyFileResources\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"bulkDestroyFileResources\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/file_resources/bulk_destroy`, { body });\n}\n\n/**\n * Shows a file resource\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function showFileResource(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"showFileResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/file_resources/${id}`);\n}\n\n/**\n * Deletes a file resource\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function destroyFileResource(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"destroyFileResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/file_resources/${id}`);\n}\n\n// ============================================================================\n// root-themes\n// ============================================================================\n\n/**\n * List root themes\n * Get all root themes with optional filters\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listRootThemes(\n client: FetchClient,\n params?: operations[\"listRootThemes\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listRootThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/root_themes`, params);\n}\n\n/**\n * Create a root theme\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createRootTheme(\n client: FetchClient,\n body: NonNullable<\n operations[\"createRootTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createRootTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/root_themes`, body);\n}\n\n/**\n * List company root themes\n * Get all company root themes with optional filters\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listCompanyRootThemes(\n client: FetchClient,\n params?: operations[\"listCompanyRootThemes\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listCompanyRootThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/root_themes/my`, params);\n}\n\n/**\n * Update a root theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateRootTheme(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateRootTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateRootTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/root_themes/${id}`, body);\n}\n\n/**\n * Delete a root theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteRootTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"deleteRootTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/root_themes/${id}`);\n}\n\n/**\n * Update a root theme status\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateRootThemeStatus(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateRootThemeStatus\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateRootThemeStatus\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/root_themes/${id}/status`, body);\n}\n\n// ============================================================================\n// theme-region-rules\n// ============================================================================\n\n/**\n * List theme region rules\n * Retrieve a list of theme region rules for the current company\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listThemeRegionRules(\n client: FetchClient,\n params?: operations[\"listThemeRegionRules\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listThemeRegionRules\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/theme_region_rules`, params);\n}\n\n/**\n * Create theme region rule\n * Create a new theme region rule\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createThemeRegionRule(\n client: FetchClient,\n body: NonNullable<\n operations[\"createThemeRegionRule\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createThemeRegionRule\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/theme_region_rules`, body);\n}\n\n/**\n * Show theme region rule\n * Retrieve a specific theme region rule\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeRegionRule(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeRegionRule\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/theme_region_rules/${id}`);\n}\n\n/**\n * Update theme region rule\n * Update an existing theme region rule\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateThemeRegionRule(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateThemeRegionRule\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateThemeRegionRule\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/theme_region_rules/${id}`, body);\n}\n\n/**\n * Delete theme region rule\n * Delete a theme region rule\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteThemeRegionRule(\n client: FetchClient,\n id: string | number,\n): Promise<void> {\n return client.delete(`/api/theme_region_rules/${id}`);\n}\n","import { isApiError } from \"@fluid-app/themes-api-client\";\n\n// Name of the bundled skill that walks the caller through migrating\n// per-template `styles.css` files to theme-level assets. See\n// `skills/template-stylesheet-to-asset-migration/SKILL.md` in this\n// package.\nexport const STYLESHEET_MIGRATION_SKILL =\n \"template-stylesheet-to-asset-migration\";\n\n// Detect the backend's 422 rejection for legacy stylesheet keys\n// (`ApplicationThemeResources::UpdateAction#stylesheet_rejected_response`\n// on the Rails side). Both the message and the errors payload identify\n// the rejection uniquely; match on either so a future wording tweak on\n// one surface does not silently drop the hint.\nfunction isStylesheetKeyRejection(error: {\n status: number;\n message: string;\n data: unknown;\n}): boolean {\n if (error.status !== 422) return false;\n if (/stylesheet.*no longer accepted/i.test(error.message)) return true;\n if (error.data && typeof error.data === \"object\") {\n const resourceErrors = (\n error.data as { application_theme_resource?: { key?: unknown } }\n ).application_theme_resource;\n if (\n resourceErrors &&\n typeof resourceErrors.key === \"string\" &&\n /stylesheet.*no longer accepted/i.test(resourceErrors.key)\n ) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Extract a human-readable message from a caught error. ApiError's default\n * `toString` prefixes with `ApiError:` and the class name; this returns the\n * API's own `error_message` verbatim so the CLI surfaces messages like\n * \"This stylesheet key is no longer accepted. Upload stylesheets as\n * theme-level assets.\" directly.\n *\n * When the error is the legacy stylesheet key rejection specifically,\n * a hint is appended pointing to the bundled migration skill — the\n * caller needs to move the per-template `styles.css` bytes to `assets/`\n * and stop pushing the deprecated column key.\n */\nexport function formatError(e: unknown): string {\n if (isApiError(e)) {\n const status = e.status ? ` [${e.status}]` : \"\";\n const hint = isStylesheetKeyRejection(e)\n ? `\\n ↳ Run \\`fluid theme skills install\\` — the bundled \\`${STYLESHEET_MIGRATION_SKILL}\\` skill can help you migrate this to a theme-level asset.`\n : \"\";\n return `${e.message}${status}${hint}`;\n }\n if (e instanceof Error) return e.message;\n return String(e);\n}\n","import { relative, sep } from \"node:path\";\nimport chokidar from \"chokidar\";\nimport type { ThemeRoot } from \"../root.js\";\nimport type { ThemeFile } from \"../file.js\";\nimport { formatError } from \"../format-error.js\";\n\nexport type FileChangeHandler = (\n modified: ThemeFile[],\n added: ThemeFile[],\n removed: ThemeFile[],\n /** When the filesystem event arrived, not when the handler got to run.\n * Handlers are serialized behind awaited uploads, so a queued event can\n * start long after it happened — anything deciding where one edit ends and\n * the next begins has to use this, or it measures upload duration. */\n arrivedAt: number,\n) => Promise<void>;\n\nfunction relativeThemePath(root: ThemeRoot, filePath: string): string {\n return relative(root.root, filePath).split(sep).join(\"/\");\n}\n\nexport function watchTheme(\n root: ThemeRoot,\n handler: FileChangeHandler,\n): () => Promise<void> {\n const watcher = chokidar.watch(root.root, {\n ignoreInitial: true,\n ignored: (filePath: string) => {\n if (filePath.includes(\"node_modules\")) return true;\n try {\n const rel = relativeThemePath(root, filePath);\n const basename = rel.split(/[\\\\/]/).pop() ?? \"\";\n return basename.startsWith(\".\") || root.ignore.ignore(rel);\n } catch {\n return false;\n }\n },\n persistent: true,\n awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 },\n });\n\n let pending = Promise.resolve();\n const enqueue = (fn: () => Promise<void>) => {\n // The change handler has its own internal try/catch around\n // per-file uploads/deletes, but anything that throws outside\n // that (e.g. `root.file()`, `validateSchema()`) must still be\n // surfaced — a bare `.catch(() => {})` here previously swallowed\n // it and left watch mode silently stuck.\n pending = pending.then(fn).catch((e) => {\n console.error(` [Watcher] change handling failed: ${formatError(e)}`);\n });\n };\n\n watcher.on(\"change\", (filePath) => {\n const rel = relativeThemePath(root, filePath);\n if (!root.isResourcePath(filePath) || root.ignore.ignore(rel)) return;\n const arrivedAt = Date.now();\n enqueue(() => handler([root.file(filePath)], [], [], arrivedAt));\n });\n\n watcher.on(\"add\", (filePath) => {\n const rel = relativeThemePath(root, filePath);\n if (!root.isResourcePath(filePath) || root.ignore.ignore(rel)) return;\n const arrivedAt = Date.now();\n enqueue(() => handler([], [root.file(filePath)], [], arrivedAt));\n });\n\n watcher.on(\"unlink\", (filePath) => {\n const rel = relativeThemePath(root, filePath);\n if (!root.isResourcePath(filePath) || root.ignore.ignore(rel)) return;\n const arrivedAt = Date.now();\n enqueue(() => handler([], [], [root.file(filePath)], arrivedAt));\n });\n\n return () => watcher.close();\n}\n","import { normalizeThemeResourceKey } from \"./resource-key.js\";\n\nexport class CaseCollisionError extends Error {\n constructor(readonly collisions: readonly (readonly string[])[]) {\n super(\n `Theme contains paths that differ only by letter case and cannot be synchronized safely:\\n${collisions\n .map((paths) => ` ${paths.join(\", \")}`)\n .join(\"\\n\")}`,\n );\n this.name = \"CaseCollisionError\";\n }\n}\n\n/** Refuse paths a case-insensitive checkout cannot represent independently. */\nexport function assertNoCaseCollisions(paths: readonly string[]): void {\n const pathsByFoldedKey = new Map<string, Set<string>>();\n for (const rawPath of paths) {\n const path = normalizeThemeResourceKey(rawPath);\n const foldedKey = path.toLowerCase();\n const matchingPaths = pathsByFoldedKey.get(foldedKey) ?? new Set<string>();\n matchingPaths.add(path);\n pathsByFoldedKey.set(foldedKey, matchingPaths);\n }\n\n const collisions = [...pathsByFoldedKey.values()]\n .filter((matchingPaths) => matchingPaths.size > 1)\n .map((matchingPaths) => [...matchingPaths].sort())\n .sort(([left = \"\"], [right = \"\"]) => left.localeCompare(right));\n\n if (collisions.length > 0) throw new CaseCollisionError(collisions);\n}\n","import { createHash, randomBytes } from \"node:crypto\";\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nconst MANIFEST_FILE = \".fluid-assets.json\";\nconst MANIFEST_VERSION = 1;\n\n/**\n * A tiny shadow-repo placeholder for a manifest-backed asset. It preserves\n * the path as a deletion baseline without retaining the asset bytes locally.\n */\nexport const MANAGED_ASSET_SHADOW_SENTINEL = \"fluid-managed-asset\\n\";\n\nexport interface ThemeAssetLink {\n /** Theme that last supplied this asset reference. */\n sourceThemeId: number;\n /** SHA-256 checksum of the source resource, when the API provides one. */\n checksum?: string;\n /** ImageKit URL saved into themes that consume this asset. */\n url?: string;\n /** MIME type required to create a URL-backed FileResource. */\n contentType?: string;\n /** File size required to create a URL-backed FileResource. */\n contentSize?: number;\n /** Optional display metadata copied with the ImageKit URL. */\n previewImageUrl?: string;\n altText?: string;\n handle?: string;\n /** A dev-only source that must survive pull until a push makes it durable. */\n pending?: boolean;\n /** DAM asset identity retained as provenance for CLI-uploaded assets. */\n damAssetCode?: string;\n}\n\ninterface ThemeAssetManifestDocument {\n version: number;\n assets: Record<string, ThemeAssetLink>;\n}\n\n/**\n * Tracks binary theme assets that deliberately live only on the server.\n *\n * The manifest is a dotfile so it is excluded from theme uploads and file\n * watching. It is written before the corresponding local file is removed,\n * which prevents `delete: true` from mistaking the removed byte source for a\n * request to delete its remote FileResource.\n */\nexport class ThemeAssetManifest {\n private assets: Record<string, ThemeAssetLink>;\n private readonly path: string;\n\n constructor(themeRoot: string) {\n this.path = join(themeRoot, MANIFEST_FILE);\n this.assets = readDocument(this.path).assets;\n }\n\n reload(): void {\n this.assets = readDocument(this.path).assets;\n }\n\n keys(): string[] {\n return Object.keys(this.assets);\n }\n\n entries(): Array<[string, ThemeAssetLink]> {\n return Object.entries(this.assets).map(([key, link]) => [\n key,\n copyLink(link),\n ]);\n }\n\n /**\n * Stable digest of the URL references represented by this manifest.\n * Pull baselines exclude pending entries because those belong only to an\n * existing dev target and are deliberately absent from the pulled source.\n */\n fingerprint(opts: { excludePending?: boolean } = {}): string {\n const entries = this.entries()\n .filter(([, link]) => !opts.excludePending || !link.pending)\n .toSorted(([left], [right]) =>\n left < right ? -1 : left > right ? 1 : 0,\n );\n return createHash(\"sha256\").update(JSON.stringify(entries)).digest(\"hex\");\n }\n\n has(key: string): boolean {\n return this.assets[key] !== undefined;\n }\n\n get(key: string): ThemeAssetLink | undefined {\n const link = this.assets[key];\n return link ? copyLink(link) : undefined;\n }\n\n set(key: string, link: ThemeAssetLink): void {\n if (!isThemeAssetKey(key) || !isThemeAssetLink(link)) {\n throw new Error(`invalid asset entry for ${key}`);\n }\n this.assets[key] = copyLink(link);\n }\n\n delete(key: string): void {\n delete this.assets[key];\n }\n\n write(): void {\n const document: ThemeAssetManifestDocument = {\n version: MANIFEST_VERSION,\n assets: copyAssets(this.assets),\n };\n const tempPath = `${this.path}.${randomBytes(6).toString(\"hex\")}.tmp`;\n\n try {\n mkdirSync(dirname(this.path), { recursive: true });\n writeFileSync(tempPath, JSON.stringify(document, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n renameSync(tempPath, this.path);\n } catch (error) {\n try {\n unlinkSync(tempPath);\n } catch {\n // The temporary file may not have been written yet.\n }\n throw error;\n }\n }\n}\n\nfunction readDocument(path: string): ThemeAssetManifestDocument {\n if (!existsSync(path)) return emptyDocument();\n\n try {\n return parseDocument(JSON.parse(readFileSync(path, \"utf-8\")));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Could not read ${MANIFEST_FILE}: ${message}`);\n }\n}\n\nfunction parseDocument(value: unknown): ThemeAssetManifestDocument {\n if (!isRecord(value) || value[\"version\"] !== MANIFEST_VERSION) {\n throw new Error(`expected version ${MANIFEST_VERSION}`);\n }\n\n const rawAssets = value[\"assets\"];\n if (!isRecord(rawAssets)) throw new Error(\"expected an assets object\");\n\n const assets: Record<string, ThemeAssetLink> = {};\n for (const [key, rawLink] of Object.entries(rawAssets)) {\n if (!isThemeAssetKey(key) || !isThemeAssetLink(rawLink)) {\n throw new Error(`invalid asset entry for ${key}`);\n }\n assets[key] = copyLink(rawLink);\n }\n\n return { version: MANIFEST_VERSION, assets };\n}\n\nfunction emptyDocument(): ThemeAssetManifestDocument {\n return { version: MANIFEST_VERSION, assets: {} };\n}\n\nfunction copyAssets(\n assets: Record<string, ThemeAssetLink>,\n): Record<string, ThemeAssetLink> {\n return Object.fromEntries(\n Object.entries(assets).map(([key, link]) => [key, copyLink(link)]),\n );\n}\n\nfunction copyLink(link: ThemeAssetLink): ThemeAssetLink {\n return {\n sourceThemeId: link.sourceThemeId,\n ...(typeof link.checksum === \"string\" ? { checksum: link.checksum } : {}),\n ...(typeof link.url === \"string\" ? { url: link.url } : {}),\n ...(typeof link.contentType === \"string\"\n ? { contentType: link.contentType }\n : {}),\n ...(typeof link.contentSize === \"number\"\n ? { contentSize: link.contentSize }\n : {}),\n ...(typeof link.previewImageUrl === \"string\"\n ? { previewImageUrl: link.previewImageUrl }\n : {}),\n ...(typeof link.altText === \"string\" ? { altText: link.altText } : {}),\n ...(typeof link.handle === \"string\" ? { handle: link.handle } : {}),\n ...(link.pending === true ? { pending: true } : {}),\n ...(typeof link.damAssetCode === \"string\"\n ? { damAssetCode: link.damAssetCode }\n : {}),\n };\n}\n\nfunction isThemeAssetLink(value: unknown): value is ThemeAssetLink {\n return (\n isRecord(value) &&\n typeof value[\"sourceThemeId\"] === \"number\" &&\n Number.isInteger(value[\"sourceThemeId\"]) &&\n value[\"sourceThemeId\"] > 0 &&\n (value[\"checksum\"] === undefined ||\n (typeof value[\"checksum\"] === \"string\" &&\n value[\"checksum\"].length > 0)) &&\n (value[\"url\"] === undefined ||\n (typeof value[\"url\"] === \"string\" && value[\"url\"].length > 0)) &&\n (value[\"contentType\"] === undefined ||\n (typeof value[\"contentType\"] === \"string\" &&\n value[\"contentType\"].length > 0)) &&\n (value[\"contentSize\"] === undefined ||\n (typeof value[\"contentSize\"] === \"number\" &&\n Number.isInteger(value[\"contentSize\"]) &&\n value[\"contentSize\"] > 0)) &&\n (value[\"previewImageUrl\"] === undefined ||\n (typeof value[\"previewImageUrl\"] === \"string\" &&\n value[\"previewImageUrl\"].length > 0)) &&\n (value[\"altText\"] === undefined || typeof value[\"altText\"] === \"string\") &&\n (value[\"handle\"] === undefined ||\n (typeof value[\"handle\"] === \"string\" && value[\"handle\"].length > 0)) &&\n (value[\"pending\"] === undefined || typeof value[\"pending\"] === \"boolean\") &&\n (value[\"damAssetCode\"] === undefined ||\n (typeof value[\"damAssetCode\"] === \"string\" &&\n value[\"damAssetCode\"].length > 0))\n );\n}\n\nexport function isThemeAssetKey(key: string): boolean {\n if (key.includes(\"\\\\\") || key.includes(\"\\0\")) return false;\n\n const segments = key.split(\"/\");\n return (\n segments[0] === \"assets\" &&\n segments.length === 2 &&\n segments[1] !== undefined &&\n segments[1].length > 0 &&\n segments[1] !== \".\" &&\n segments[1] !== \"..\"\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n","// Resource keys that the backend can hide from the resources index\n// when `STYLESHEET_STRICT_INPUT` is enabled on the owning company —\n// theme-level `styles.css` / `global_styles.css` and per-template\n// composite `{type}/{name}/styles.css`. See `Themes::Resource.find_by`\n// + `Themes::Theme#theme_stylesheet_columns_hidden?` /\n// `Themes::Template#stylesheet_column_hidden?` on the Rails side.\n//\n// A pull that used the \"delete files missing on remote\" behavior would\n// otherwise wipe a merchant's local stylesheets the first time the\n// flag flips on — the resource just stops appearing in the index, not\n// the file itself. Skip these keys from the delete pass so local\n// content stays intact regardless of the flag state.\n\nconst STYLESHEET_KEY_PATTERN =\n /^(styles\\.css|global_styles\\.css|[^/]+\\/[^/]+\\/styles\\.css)$/;\n\nexport function isStylesheetKey(key: string): boolean {\n return STYLESHEET_KEY_PATTERN.test(key);\n}\n","import { unlinkSync } from \"node:fs\";\nimport { sep } from \"node:path\";\nimport {\n isApiError,\n themes,\n type components,\n} from \"@fluid-app/themes-api-client\";\nimport type { ApiClient } from \"../api.js\";\nimport { formatError } from \"./format-error.js\";\nimport type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport { assertNoCaseCollisions } from \"./case-collisions.js\";\nimport {\n isThemeAssetKey,\n ThemeAssetManifest,\n type ThemeAssetLink,\n} from \"./asset-manifest.js\";\nimport { isStylesheetKey } from \"./stylesheet-keys.js\";\nimport type {\n DevRemoteState,\n RemoteResourceState,\n} from \"./dev-remote-baseline.js\";\n\ntype RemoteResource = components[\"schemas\"][\"ApplicationThemeResource\"];\ninterface RemoteAssetMetadata {\n url: string;\n contentType: string;\n contentSize: number;\n previewImageUrl?: string;\n altText?: string;\n handle?: string;\n}\n\ninterface ManagedAssetPlan {\n key: string;\n link: ThemeAssetLink;\n targetResource?: RemoteResource;\n metadata?: RemoteAssetMetadata;\n}\n\ntype UploadedBinaryResource = RemoteResource & {\n damAssetCode?: string;\n assetMetadata?: RemoteAssetMetadata;\n};\n\nconst ASSET_REFERENCE_CONCURRENCY = 6;\n\nexport interface SyncResult {\n uploaded: number;\n downloaded: number;\n linked: number;\n deleted: number;\n errors: string[];\n validationFailed: boolean;\n}\n\n/**\n * Server rejected the push because the CLI's `base_sha` no longer\n * matches the theme's current `content_version_sha` (someone else\n * wrote to the theme since our last pull). Callers surface a\n * \"pull first\" message; no partial state has been written when this\n * throws from the preflight, and the per-file variant preserves the\n * atomicity guarantee mid-loop by short-circuiting the remaining\n * files on the first conflicting response.\n */\nexport class PushConflictError extends Error {\n constructor(public readonly remoteSha: string | null) {\n super(\n remoteSha\n ? `Your local is behind the server. Server is at ${remoteSha}; local is stale.`\n : \"Your local is behind the server.\",\n );\n this.name = \"PushConflictError\";\n }\n}\n\nexport class Syncer {\n private checksumIndex = new Map<string, string>();\n private rawRemoteResources = new Map<string, RemoteResource>();\n private remoteResourceGroups = new Map<string, RemoteResource[]>();\n private remoteResourceIndex = new Map<string, RemoteResource>();\n private remoteIndexesDirty = false;\n private remoteResourcesLoaded = false;\n private lastKnownRemoteSha: string | null = null;\n private assetManifestInstance: ThemeAssetManifest | undefined;\n\n constructor(\n private api: ApiClient,\n private themeId: number,\n private themeRoot: ThemeRoot,\n assetManifest?: ThemeAssetManifest,\n ) {\n this.assetManifestInstance = assetManifest;\n }\n\n private get assetManifest(): ThemeAssetManifest {\n this.assetManifestInstance ??= new ThemeAssetManifest(this.themeRoot.root);\n return this.assetManifestInstance;\n }\n\n // ─── Checksum Management ──────────────────────────────────────────────────\n\n async fetchChecksums(): Promise<void> {\n // `content_version_sha` on the resources index is a Phase 003a\n // server addition — older servers don't emit it. Cast through\n // `unknown` because the typed API client hasn't been regenerated\n // against the new OpenAPI spec yet; regeneration is a follow-up.\n const body = (await themes.listThemeResources(\n this.api,\n this.themeId,\n )) as unknown as {\n application_theme_resources?: RemoteResource[];\n content_version_sha?: string;\n };\n this.updateChecksums(body.application_theme_resources ?? []);\n this.lastKnownRemoteSha = body.content_version_sha ?? null;\n this.remoteResourcesLoaded = true;\n }\n\n /**\n * Server's `content_version_sha` captured on the last `fetchChecksums()`\n * or `downloadAll()`. `null` when talking to a pre-003a server.\n */\n remoteSha(): string | null {\n return this.lastKnownRemoteSha;\n }\n\n private updateChecksums(resources: RemoteResource[]): void {\n assertNoCaseCollisions(\n resources.flatMap((resource) => (resource.key ? [resource.key] : [])),\n );\n this.rawRemoteResources.clear();\n this.remoteResourceGroups.clear();\n for (const resource of resources) {\n if (!resource.key) continue;\n\n this.rawRemoteResources.set(resource.key, resource);\n const group = this.remoteResourceGroups.get(resource.key) ?? [];\n group.push(resource);\n this.remoteResourceGroups.set(resource.key, group);\n }\n this.remoteIndexesDirty = true;\n }\n\n private setRemoteResource(resource: RemoteResource): void {\n if (!resource.key) return;\n this.rawRemoteResources.set(resource.key, resource);\n this.remoteResourceGroups.set(resource.key, [resource]);\n this.remoteIndexesDirty = true;\n }\n\n private removeRemoteResource(relativePath: string): void {\n this.rawRemoteResources.delete(relativePath);\n this.remoteResourceGroups.delete(relativePath);\n this.remoteIndexesDirty = true;\n }\n\n // Rebuilding on every setRemoteResource/removeRemoteResource made bulk\n // upload loops O(n²) over the remote resource map, so mutations only mark\n // the indexes dirty and the next read rebuilds once.\n private get checksums(): Map<string, string> {\n if (this.remoteIndexesDirty) this.rebuildRemoteIndexes();\n return this.checksumIndex;\n }\n\n private get remoteResources(): Map<string, RemoteResource> {\n if (this.remoteIndexesDirty) this.rebuildRemoteIndexes();\n return this.remoteResourceIndex;\n }\n\n private rebuildRemoteIndexes(): void {\n this.remoteIndexesDirty = false;\n this.checksumIndex.clear();\n this.remoteResourceIndex.clear();\n\n for (const [key, resource] of this.rawRemoteResources) {\n // The backend may return both a legacy key and its .liquid equivalent.\n // Preserve the explicit .liquid resource when both are present.\n if (this.rawRemoteResources.has(`${key}.liquid`)) continue;\n\n this.remoteResourceIndex.set(key, resource);\n if (resource.checksum) this.checksumIndex.set(key, resource.checksum);\n }\n }\n\n hasChanged(file: ThemeFile): boolean {\n return file.checksum() !== this.checksums.get(file.relativePath);\n }\n\n remoteKeys(): string[] {\n return [...this.remoteResources.keys()];\n }\n\n /** A null-content resource has no possible working-tree counterpart. */\n private canDeleteRemoteResource(key: string): boolean {\n const resource = this.remoteResources.get(key);\n return resource?.content != null || isManagedAssetResource(resource);\n }\n\n /** Snapshot of remote checksums (key → sha256). Available after fetchChecksums() or downloadAll(). */\n remoteChecksums(): Record<string, string> {\n return Object.fromEntries(this.checksums);\n }\n\n /** URL-backed assets keyed by their exact theme resource path. */\n remoteAssetUrls(): Record<string, string> {\n const urls: Record<string, string> = {};\n for (const [key, resource] of this.remoteResources) {\n if (!isManagedAssetResource(resource)) continue;\n const url = resource.url;\n if (typeof url === \"string\" && url.length > 0) urls[key] = url;\n }\n return urls;\n }\n\n /** Compact, complete resource state paired with its acknowledged dev SHA. */\n devRemoteState(assetManifestSha: string): DevRemoteState | null {\n if (!this.remoteResourcesLoaded || !this.lastKnownRemoteSha) return null;\n\n return {\n themeId: this.themeId,\n remoteSha: this.lastKnownRemoteSha,\n assetManifestSha,\n resources: [...this.remoteResourceGroups.values()]\n .flat()\n .map(remoteResourceState),\n };\n }\n\n useDevRemoteState(state: DevRemoteState): void {\n if (state.themeId !== this.themeId) {\n throw new Error(\n `Dev remote state belongs to theme #${state.themeId}, not #${this.themeId}`,\n );\n }\n this.updateChecksums(state.resources.map(remoteResourceFromState));\n this.lastKnownRemoteSha = state.remoteSha;\n this.remoteResourcesLoaded = true;\n }\n\n private async ensureRemoteResourcesLoaded(): Promise<void> {\n if (!this.remoteResourcesLoaded) await this.fetchChecksums();\n }\n\n /**\n * Adds URL-backed FileResources for manifest assets without transferring\n * their bytes. The target stores the source asset's ImageKit URL.\n */\n async linkManagedAssets(opts: { replace?: boolean } = {}): Promise<number> {\n this.assetManifest.reload();\n await this.ensureRemoteResourcesLoaded();\n\n const plans: ManagedAssetPlan[] = [];\n for (const [key, link] of this.assetManifest.entries()) {\n if (this.themeRoot.ignore.ignore(key)) continue;\n // A local binary is the recovery source after a failed metadata pull or\n // an intentional replacement. Let the normal upload path handle it;\n // never recreate the older manifest URL over those bytes first.\n if (this.themeRoot.file(key).exists) continue;\n\n const targetResource =\n this.managedAssetResourceForLink(key, link) ??\n this.remoteResources.get(key);\n if (\n targetResource &&\n !this.managedAssetNeedsRefresh(targetResource, link)\n ) {\n continue;\n }\n if (targetResource && !opts.replace) continue;\n\n const metadata = assetMetadataFromLink(link);\n plans.push({\n key,\n link,\n ...(targetResource ? { targetResource } : {}),\n ...(metadata ? { metadata } : {}),\n });\n }\n\n if (plans.length > 0) {\n await this.resolveAssetMetadata(plans);\n await this.createAssetReferences(plans);\n await this.fetchChecksums();\n }\n if (opts.replace && (await this.pruneDuplicateManagedAssetReferences())) {\n await this.fetchChecksums();\n }\n this.ensureManagedAssetsAreResolved();\n\n return plans.length;\n }\n\n private async resolveAssetMetadata(plans: ManagedAssetPlan[]): Promise<void> {\n const bySourceTheme = new Map<number, ManagedAssetPlan[]>();\n for (const plan of plans) {\n if (plan.metadata) continue;\n const sourcePlans = bySourceTheme.get(plan.link.sourceThemeId) ?? [];\n sourcePlans.push(plan);\n bySourceTheme.set(plan.link.sourceThemeId, sourcePlans);\n }\n\n let manifestChanged = false;\n for (const [sourceThemeId, sourcePlans] of bySourceTheme) {\n let sourceAssets: Map<string, RemoteAssetMetadata>;\n try {\n sourceAssets = await this.fetchThemeAssetMetadata(sourceThemeId);\n } catch (error) {\n throw new Error(\n `Could not read asset metadata from theme #${sourceThemeId}: ${formatError(error)}`,\n );\n }\n\n for (const plan of sourcePlans) {\n const sourceMetadata = sourceAssets.get(assetFilename(plan.key));\n if (!sourceMetadata) {\n throw new Error(\n `Could not find usable metadata for ${plan.key} in theme #${sourceThemeId}`,\n );\n }\n\n // Preserve a pulled URL while enriching an older manifest. A remote\n // source update must not silently change this checkout's asset URL.\n const metadata: RemoteAssetMetadata = {\n ...sourceMetadata,\n ...(typeof plan.link.url === \"string\" ? { url: plan.link.url } : {}),\n };\n plan.metadata = metadata;\n this.assetManifest.set(plan.key, {\n ...plan.link,\n ...metadata,\n });\n manifestChanged = true;\n }\n }\n\n if (manifestChanged) this.assetManifest.write();\n }\n\n private async fetchThemeAssetMetadata(\n sourceThemeId: number,\n ): Promise<Map<string, RemoteAssetMetadata>> {\n const body = await themes.getThemeAssets(this.api, sourceThemeId);\n if (!isRecord(body) || !Array.isArray(body[\"file_resources\"])) {\n throw new Error(\"Theme assets response did not include file_resources\");\n }\n\n const assets = new Map<string, RemoteAssetMetadata>();\n for (const value of body[\"file_resources\"]) {\n const asset = parseThemeAssetMetadata(value);\n if (asset) assets.set(asset.filename, asset.metadata);\n }\n return assets;\n }\n\n private async createAssetReferences(\n plans: ManagedAssetPlan[],\n ): Promise<void> {\n const errors: string[] = [];\n let nextPlan = 0;\n\n const worker = async () => {\n while (nextPlan < plans.length) {\n const plan = plans[nextPlan];\n nextPlan += 1;\n if (!plan || !plan.metadata) continue;\n\n try {\n await this.createAssetReference(plan);\n } catch (error) {\n errors.push(`${plan.key}: ${formatError(error)}`);\n }\n }\n };\n\n await Promise.all(\n Array.from(\n { length: Math.min(ASSET_REFERENCE_CONCURRENCY, plans.length) },\n worker,\n ),\n );\n\n if (errors.length > 0) {\n throw new Error(\n `Could not save ${errors.length} ImageKit URL reference(s) (this requires File Resources update access): ${errors.join(\"; \")}`,\n );\n }\n }\n\n private async createAssetReference(plan: ManagedAssetPlan): Promise<void> {\n const metadata = plan.metadata;\n if (!metadata) throw new Error(\"asset metadata was not resolved\");\n\n const targetResourceId = plan.targetResource?.resource_id;\n if (plan.targetResource && typeof targetResourceId !== \"number\") {\n throw new Error(\"existing target asset has no resource ID\");\n }\n\n const body = await themes.createFileResource(this.api, {\n file_resource: {\n url: metadata.url,\n filename: assetFilename(plan.key),\n content_type: metadata.contentType,\n content_size: metadata.contentSize,\n ...(metadata.previewImageUrl\n ? { preview_image_url: metadata.previewImageUrl }\n : {}),\n ...(metadata.altText !== undefined\n ? { alt_text: metadata.altText }\n : {}),\n ...(metadata.handle ? { handle: metadata.handle } : {}),\n relateable_id: this.themeId,\n relateable_type: \"ApplicationTheme\",\n },\n });\n\n const createdResourceId = createdFileResourceId(body);\n if (!createdResourceId) {\n throw new Error(\"create response did not include a FileResource ID\");\n }\n\n if (typeof targetResourceId !== \"number\") return;\n\n try {\n await themes.destroyFileResource(this.api, targetResourceId);\n } catch (error) {\n if (isNotFoundError(error)) return;\n try {\n await themes.destroyFileResource(this.api, createdResourceId);\n } catch {\n // Keep the original error; a later sync can repair any duplicate.\n }\n throw error;\n }\n }\n\n /**\n * Returns every resource for an exact logical key. A legacy `foo` resource\n * is hidden when `foo.liquid` exists, matching the normal remote index.\n */\n private resourcesForLogicalKey(key: string): RemoteResource[] {\n if (this.rawRemoteResources.has(`${key}.liquid`)) return [];\n return this.remoteResourceGroups.get(key) ?? [];\n }\n\n private managedAssetResourceForLink(\n key: string,\n link: ThemeAssetLink,\n ): RemoteResource | undefined {\n return this.resourcesForLogicalKey(key).find(\n (resource) =>\n isManagedAssetResource(resource) &&\n !this.managedAssetNeedsRefresh(resource, link),\n );\n }\n\n /** Make interrupted reference replacement converge to one FileResource. */\n private async pruneDuplicateManagedAssetReferences(): Promise<boolean> {\n const resourcesToDelete: Array<{ key: string; resourceId: number }> = [];\n\n for (const [key, link] of this.assetManifest.entries()) {\n if (this.themeRoot.ignore.ignore(key) || !link.url) continue;\n\n const resources = this.resourcesForLogicalKey(key).filter(\n isManagedAssetResource,\n );\n if (resources.length < 2) continue;\n\n const matchingResources = resources.filter(\n (resource) => resource.url === link.url,\n );\n if (matchingResources.length === 0) continue;\n\n const keeper = this.lowestResourceId(matchingResources, key);\n const keeperId = this.resourceIdForAssetReference(keeper, key);\n for (const resource of resources) {\n const resourceId = this.resourceIdForAssetReference(resource, key);\n if (resourceId !== keeperId)\n resourcesToDelete.push({ key, resourceId });\n }\n }\n\n if (resourcesToDelete.length === 0) return false;\n\n const errors: string[] = [];\n let nextResource = 0;\n const worker = async () => {\n while (nextResource < resourcesToDelete.length) {\n const resource = resourcesToDelete[nextResource];\n nextResource += 1;\n if (!resource) continue;\n\n try {\n await themes.destroyFileResource(this.api, resource.resourceId);\n } catch (error) {\n if (!isNotFoundError(error)) {\n errors.push(`${resource.key}: ${formatError(error)}`);\n }\n }\n }\n };\n\n await Promise.all(\n Array.from(\n {\n length: Math.min(\n ASSET_REFERENCE_CONCURRENCY,\n resourcesToDelete.length,\n ),\n },\n worker,\n ),\n );\n\n if (errors.length > 0) {\n throw new Error(\n `Could not remove ${errors.length} duplicate ImageKit URL reference(s): ${errors.join(\"; \")}`,\n );\n }\n\n return true;\n }\n\n private lowestResourceId(\n resources: RemoteResource[],\n key: string,\n ): RemoteResource {\n let lowest = resources[0];\n if (!lowest) throw new Error(`No asset resources found for ${key}`);\n\n let lowestId = this.resourceIdForAssetReference(lowest, key);\n for (const resource of resources.slice(1)) {\n const resourceId = this.resourceIdForAssetReference(resource, key);\n if (resourceId < lowestId) {\n lowest = resource;\n lowestId = resourceId;\n }\n }\n return lowest;\n }\n\n private resourceIdForAssetReference(\n resource: RemoteResource,\n key: string,\n ): number {\n const resourceId = positiveInteger(resource.resource_id);\n if (!resourceId) {\n throw new Error(`Existing target asset has no resource ID: ${key}`);\n }\n return resourceId;\n }\n\n /** Makes this target the provenance source for assets it now resolves. */\n repointManagedAssetsToCurrentTheme(): void {\n this.assetManifest.reload();\n let changed = false;\n\n for (const [key, link] of this.assetManifest.entries()) {\n if (this.themeRoot.ignore.ignore(key)) continue;\n if (this.themeRoot.file(key).exists) continue;\n\n const resource = this.remoteResources.get(key);\n if (!isManagedAssetResource(resource)) {\n throw new Error(`Managed asset is missing from target theme: ${key}`);\n }\n\n const nextLink: ThemeAssetLink = { ...link, sourceThemeId: this.themeId };\n delete nextLink.pending;\n delete nextLink.checksum;\n Object.assign(nextLink, resourceLink(resource));\n this.assetManifest.set(key, nextLink);\n changed = true;\n }\n\n if (changed) this.assetManifest.write();\n }\n\n // ─── Upload ───────────────────────────────────────────────────────────────\n\n /**\n * Uploads one file. Resolves with the exact text content that was sent to\n * the server (null for binary files) so callers can run diagnostics against\n * the same bytes instead of re-reading a file that may have changed on disk\n * while the request was in flight.\n */\n async uploadFile(\n file: ThemeFile,\n baseSha?: string | null,\n opts: { pendingAsset?: boolean } = {},\n ): Promise<string | null> {\n if (file.isText) {\n const content = file.read();\n const resource = await this.putResource(\n { key: file.relativePath, content },\n baseSha,\n );\n this.setRemoteResource({\n ...resource,\n key: file.relativePath,\n content,\n checksum: resource.checksum ?? file.checksum(),\n });\n return content;\n }\n\n if (isNestedBinaryThemeAsset(file)) {\n throw new Error(\n `Binary assets must be directly inside assets/: ${file.relativePath}`,\n );\n }\n\n const resource = await this.uploadBinaryFile(file, baseSha);\n this.setRemoteResource(resource);\n if (isThemeAssetKey(file.relativePath)) {\n this.externalizeBinaryFile(file, resource, opts.pendingAsset);\n }\n return null;\n }\n\n /**\n * Wraps the generated `updateThemeResource` client with the two\n * merge-aware Phase 003 additions: sending `base_sha` on the request\n * and reading the server's fresh `content_version_sha` off the\n * response so the caller can thread it forward on the next PUT.\n *\n * Accepts any `application_theme_resource` shape — text uploads pass\n * `{ key, content }`; binary uploads (after DAM + ImageKit\n * orchestration) pass `{ key, dam_asset: { ... } }`. Both must route\n * through here so `lastKnownRemoteSha` stays in lockstep with every\n * write the server has ack'd, mixed text/binary pushes included.\n *\n * The typed client hasn't been regenerated against the new OpenAPI\n * spec yet, so `base_sha` is threaded through as an extra property\n * (server accepts unknown fields on this endpoint) and the response\n * is cast to read the extra `content_version_sha`. Regeneration is\n * a follow-up; that PR will drop these casts.\n *\n * `PushConflictError` is thrown on a 409 so callers can distinguish\n * \"server rejected because of stale base\" from generic upload\n * failures.\n */\n private async putResource(\n resource: Record<string, unknown>,\n baseSha: string | null | undefined,\n ): Promise<RemoteResource> {\n const body: Record<string, unknown> = {\n application_theme_resource: resource,\n };\n if (baseSha) body[\"base_sha\"] = baseSha;\n\n try {\n const response = (await themes.updateThemeResource(\n this.api,\n this.themeId,\n body as never,\n )) as unknown as {\n application_theme_resource?: RemoteResource;\n content_version_sha?: string;\n };\n if (response.content_version_sha) {\n this.lastKnownRemoteSha = response.content_version_sha;\n }\n return (\n response.application_theme_resource ?? {\n key: typeof resource[\"key\"] === \"string\" ? resource[\"key\"] : \"\",\n checksum: null,\n }\n );\n } catch (e) {\n throw this.rethrowIfConflict(e);\n }\n }\n\n /**\n * Server-side push preflight (Phase 003a). Runs once at the start of\n * a push loop. On 200 the server's fresh `remote_sha` is stashed on\n * the syncer so subsequent per-file PUTs can carry it. On 409 a\n * `PushConflictError` is thrown carrying the server's `remote_sha`\n * from the `meta` payload — the caller renders \"pull first\".\n *\n * Skipped when `baseSha` is null/undefined so old-behavior pushes\n * (no stored `baseSha` in `.fluid-theme.json`) and `--force` pushes\n * short-circuit past the check.\n */\n /**\n * Tell Fluid the push is finished, so it commits the theme's current state\n * as one version.\n *\n * The other half of `preflightPush`. That one runs once before the file\n * loop to reject a stale base; this runs once after it, and is the only\n * thing that turns a push into a commit — the per-file writes just mark the\n * theme changed.\n *\n * It exists because the server cannot see where an operation ends. A push\n * arrives as a hundred-odd independent requests, and every way of inferring\n * \"these belong together\" either merges two publishes that happened to land\n * close together or splits one push across several commits. The client\n * knows; this says so.\n *\n * Best-effort in that it never throws: the files are already on the server\n * by the time this runs, and Fluid sweeps anything left unsynced, so a\n * failure delays the commit rather than losing it and must not fail the\n * push.\n *\n * Returns whether Fluid took the request, because a caller tracking edit\n * boundaries needs to know. A failed ask leaves the previous edit\n * uncommitted, and uploading the next one over it merges the two into\n * whichever commit eventually lands.\n */\n async requestSync(): Promise<boolean> {\n try {\n const response = (await this.api.post(\n `/api/application_themes/${this.themeId}/resources/sync`,\n {},\n )) as { content_version_sha?: string };\n if (response.content_version_sha) {\n this.lastKnownRemoteSha = response.content_version_sha;\n }\n return true;\n } catch (e) {\n console.warn(\n ` ⚠ couldn't ask Fluid to commit this push (${formatError(e)}). It will be picked up automatically.`,\n );\n return false;\n }\n }\n\n async preflightPush(baseSha: string | null | undefined): Promise<void> {\n if (!baseSha) return;\n\n try {\n const response = (await this.api.post(\n `/api/application_themes/${this.themeId}/resources/check_push`,\n { base_sha: baseSha },\n )) as { remote_sha?: string };\n if (response.remote_sha) this.lastKnownRemoteSha = response.remote_sha;\n } catch (e) {\n throw this.rethrowIfConflict(e);\n }\n }\n\n private rethrowIfConflict(e: unknown): unknown {\n const status =\n (e as { status?: number; response?: { status?: number } })?.status ??\n (e as { response?: { status?: number } })?.response?.status;\n if (status !== 409) return e;\n\n // `ApiError` from api-client-core stores the parsed response body\n // under `.data` (not `.body`). Reading the wrong key silently\n // dropped `remote_sha` in production so the \"server is at X\"\n // hint in the CLI output went missing.\n const meta = (e as { data?: { meta?: { remote_sha?: string } } })?.data\n ?.meta;\n return new PushConflictError(meta?.remote_sha ?? null);\n }\n\n private async uploadBinaryFile(\n file: ThemeFile,\n baseSha: string | null | undefined,\n ): Promise<UploadedBinaryResource> {\n // Step 1: Create DAM placeholder\n const placeholderBody = await this.api.post<{\n asset: { id: number; canonical_path: string };\n }>(\"/api/dam/assets\", {\n placeholder_asset: {\n description: `Uploaded via Fluid CLI: ${file.name}`,\n mime_type: file.mime.name,\n name: file.name,\n },\n });\n const asset = placeholderBody.asset;\n\n // Step 2: Get ImageKit auth token\n const authBody = await this.api.post<{\n token: string;\n signature: string;\n expire: number;\n }>(\"/api/dam/assets/imagekit_auth\", {});\n\n // Step 3: Upload to ImageKit via multipart\n const folder = this.canonicalPathToImageKitFolder(asset.canonical_path);\n const formData = new FormData();\n const blob = new Blob([file.readBinary() as unknown as ArrayBuffer], {\n type: file.mime.name,\n });\n formData.append(\"file\", blob, file.name);\n formData.append(\"token\", authBody.token);\n formData.append(\"signature\", authBody.signature);\n formData.append(\"expire\", String(authBody.expire));\n formData.append(\"folder\", folder);\n formData.append(\"fileName\", file.name);\n formData.append(\"publicKey\", \"public_j7s4Ih9ETh/OCp41mVQH7tlXBdU=\");\n\n const ikResp = await fetch(\n \"https://upload.imagekit.io/api/v1/files/upload\",\n {\n method: \"POST\",\n body: formData,\n },\n );\n if (!ikResp.ok) throw new Error(`ImageKit upload failed: ${ikResp.status}`);\n const ikBody = (await ikResp.json()) as {\n fileId: string;\n url: string;\n thumbnailUrl: string;\n size: number;\n height?: number;\n width?: number;\n };\n\n // Step 4: Backfill DAM asset\n const backfillPayload: Record<string, unknown> = {\n asset: {\n id: asset.id,\n imagekit_file_id: ikBody.fileId,\n imagekit_url: ikBody.url,\n mime_type: file.mime.name,\n name: file.name,\n file_size: ikBody.size,\n expected_path: asset.canonical_path,\n },\n };\n if (ikBody.height)\n (backfillPayload[\"asset\"] as Record<string, unknown>)[\"height\"] =\n ikBody.height;\n if (ikBody.width)\n (backfillPayload[\"asset\"] as Record<string, unknown>)[\"width\"] =\n ikBody.width;\n\n const backfillBody = await this.api.post<{\n asset: { code: string; default_variant_url: string };\n }>(\"/api/dam/assets/backfill_imagekit\", backfillPayload);\n\n // Step 5: Associate with theme resource. Route through `putResource`\n // so `base_sha` gets sent and `lastKnownRemoteSha` advances just\n // like text-file writes — a mixed text+binary push must keep them\n // in a single monotonic chain.\n const update = await this.putResource(\n {\n key: file.relativePath,\n dam_asset: {\n dam_asset_code: backfillBody.asset.code,\n content_type: file.mime.name,\n content_size: ikBody.size,\n filename: file.name,\n handle: backfillBody.asset.code,\n url: backfillBody.asset.default_variant_url,\n preview_image_url: ikBody.thumbnailUrl,\n },\n },\n baseSha,\n );\n\n // Older API responses can omit URL fields. Preserve the known DAM URL so\n // the manifest stays usable after a successful remote write.\n return {\n ...update,\n key: update.key || file.relativePath,\n url: update.url ?? backfillBody.asset.default_variant_url,\n damAssetCode: backfillBody.asset.code,\n assetMetadata: {\n url: backfillBody.asset.default_variant_url,\n contentType: file.mime.name,\n contentSize: ikBody.size,\n previewImageUrl: ikBody.thumbnailUrl,\n altText: file.name,\n handle: backfillBody.asset.code,\n },\n };\n }\n\n private externalizeBinaryFile(\n file: ThemeFile,\n resource: UploadedBinaryResource,\n pending: boolean | undefined,\n ): void {\n // Persist before unlinking. If persistence fails, retain the local bytes so\n // the developer can retry instead of losing the only provenance record.\n this.assetManifest.reload();\n this.assetManifest.set(file.relativePath, {\n sourceThemeId: this.themeId,\n ...resourceLink(resource),\n ...uploadedAssetMetadata(file, resource),\n ...(pending ? { pending: true } : {}),\n ...(typeof resource.damAssetCode === \"string\"\n ? { damAssetCode: resource.damAssetCode }\n : {}),\n });\n this.assetManifest.write();\n unlinkSync(file.absolutePath);\n }\n\n private canonicalPathToImageKitFolder(canonicalPath: string): string {\n const parts = canonicalPath.split(\".\");\n const companyId = parts[0] ?? \"unknown\";\n const category = parts[1] ?? \"files\";\n const assetCode = parts[2] ?? \"unknown\";\n const folderMap: Record<string, string> = {\n images: \"images\",\n videos: \"videos\",\n audio: \"audio\",\n documents: \"documents\",\n files: \"files\",\n };\n return `${companyId}/${folderMap[category] ?? \"files\"}/${assetCode}`;\n }\n\n // ─── Delete ───────────────────────────────────────────────────────────────\n\n async deleteRemoteFile(\n relativePath: string,\n baseSha?: string | null,\n ): Promise<void> {\n // Chokidar observes the intentional unlink after a binary has been\n // externalized. That unlink must never delete the server-side DAM link.\n this.assetManifest.reload();\n if (this.assetManifest.has(relativePath)) return;\n\n const body: Record<string, unknown> = {\n application_theme_resource: { key: relativePath },\n };\n if (baseSha) body[\"base_sha\"] = baseSha;\n\n try {\n const response = (await themes.deleteThemeResource(\n this.api,\n this.themeId,\n body as never,\n )) as unknown as { content_version_sha?: string };\n if (response.content_version_sha) {\n this.lastKnownRemoteSha = response.content_version_sha;\n }\n } catch (e) {\n if (!isNotFoundError(e)) throw this.rethrowIfConflict(e);\n }\n this.removeRemoteResource(relativePath);\n }\n\n // ─── Download ─────────────────────────────────────────────────────────────\n\n async downloadAll(): Promise<RemoteResource[]> {\n // Same cast rationale as `fetchChecksums`. The additional\n // `content_version_sha` field is stashed on the syncer so callers\n // (pull command) can persist it as the new `baseSha`.\n const body = (await themes.listThemeResources(\n this.api,\n this.themeId,\n )) as unknown as {\n application_theme_resources?: RemoteResource[];\n content_version_sha?: string;\n };\n const resources = body.application_theme_resources ?? [];\n this.updateChecksums(resources);\n this.lastKnownRemoteSha = body.content_version_sha ?? null;\n this.remoteResourcesLoaded = true;\n return resources;\n }\n\n async downloadBinaryAsset(url: string): Promise<Buffer> {\n const resp = await fetch(url);\n if (!resp.ok) throw new Error(`Failed to download asset: ${resp.status}`);\n return Buffer.from(await resp.arrayBuffer());\n }\n\n /**\n * Move directly-addressable binary `assets/*` resources into the local\n * manifest before merge-pull sees them. This prevents a byte download and\n * keeps those paths out of the shadow repository; their canonical state is\n * the ImageKit URL, not a local file.\n */\n async externalizePulledAssets(\n resources: RemoteResource[],\n opts: { delete: boolean },\n ): Promise<{ managedKeys: Set<string>; linked: number; errors: string[] }> {\n this.assetManifest.reload();\n\n const managedKeys = new Set<string>();\n const changedManifestKeys = new Set<string>();\n const remoteManagedKeys = new Set<string>();\n const preservedManifestKeys = new Set<string>();\n const filesToRemove = new Map<string, ThemeFile>();\n const errors: string[] = [];\n let manifestChanged = false;\n\n // ApplicationThemeResource deliberately exposes only the URL/checksum.\n // Capture FileResource metadata during the pull too, so a later push can\n // recreate the reference even if this source/development theme has since\n // been deleted. This is one small JSON request, never a binary download.\n const resourcesNeedingMetadata = resources.filter((resource) => {\n const key = resource.key;\n if (!key) return false;\n\n const file = this.themeRoot.file(key);\n if (!this.isSafeThemeFile(key, file)) return false;\n if (!isLinkableBinaryResource(resource, key, file)) return false;\n\n const existing = this.assetManifest.get(key);\n if (this.themeRoot.ignore.ignore(key) || existing?.pending) return false;\n return (\n !existing ||\n existing.url !== resource.url ||\n !assetMetadataFromLink(existing)\n );\n });\n let assetMetadata = new Map<string, RemoteAssetMetadata>();\n const unresolvedMetadataKeys = new Set<string>();\n if (resourcesNeedingMetadata.length > 0) {\n try {\n assetMetadata = await this.fetchThemeAssetMetadata(this.themeId);\n } catch (error) {\n errors.push(`Read remote asset metadata: ${formatError(error)}`);\n for (const resource of resourcesNeedingMetadata) {\n if (resource.key) unresolvedMetadataKeys.add(resource.key);\n }\n }\n\n if (unresolvedMetadataKeys.size === 0) {\n for (const resource of resourcesNeedingMetadata) {\n const key = resource.key;\n if (!key || assetMetadata.has(assetFilename(key))) continue;\n unresolvedMetadataKeys.add(key);\n errors.push(\n `Could not find usable metadata for ${key} in theme #${this.themeId}`,\n );\n }\n }\n }\n\n for (const resource of resources) {\n const key = resource.key;\n if (!key) continue;\n\n const file = this.themeRoot.file(key);\n if (!this.isSafeThemeFile(key, file)) continue;\n if (!isLinkableBinaryResource(resource, key, file)) continue;\n\n remoteManagedKeys.add(key);\n if (this.themeRoot.ignore.ignore(key)) {\n if (this.assetManifest.has(key)) preservedManifestKeys.add(key);\n managedKeys.add(key);\n continue;\n }\n\n // A newly added dev asset has not necessarily been promoted to the\n // pulled source yet. It remains authoritative until a push makes it\n // durable on the selected target.\n if (this.assetManifest.get(key)?.pending) {\n preservedManifestKeys.add(key);\n managedKeys.add(key);\n continue;\n }\n\n // Do not create an incomplete URL-only reference: a later push may need\n // content type and size after this source theme no longer exists. Let\n // merge-pull retain/download just this binary instead.\n if (unresolvedMetadataKeys.has(key)) {\n // The fallback bytes are now the only trustworthy copy. A manifest\n // entry pointing at a URL the remote no longer serves is stale —\n // keeping it would let linkManagedAssets restore the older asset if\n // the local file later disappears.\n const stale = this.assetManifest.get(key);\n if (stale && stale.url !== resource.url) {\n this.assetManifest.delete(key);\n manifestChanged = true;\n }\n continue;\n }\n\n try {\n const existing = this.assetManifest.get(key);\n const metadata = assetMetadata.get(assetFilename(key));\n this.assetManifest.set(key, {\n ...existing,\n sourceThemeId: this.themeId,\n ...metadata,\n ...resourceLink(resource),\n });\n manifestChanged = true;\n changedManifestKeys.add(key);\n managedKeys.add(key);\n filesToRemove.set(key, file);\n } catch (error) {\n errors.push(`Externalize ${key}: ${formatError(error)}`);\n }\n }\n\n if (opts.delete) {\n for (const [key, link] of this.assetManifest.entries()) {\n if (\n remoteManagedKeys.has(key) ||\n preservedManifestKeys.has(key) ||\n this.themeRoot.ignore.ignore(key) ||\n link.pending\n ) {\n continue;\n }\n this.assetManifest.delete(key);\n manifestChanged = true;\n }\n }\n\n if (manifestChanged) {\n try {\n // Persist all links first. If this fails, preserve the local bytes and\n // let merge-pull materialize the affected resources normally.\n this.assetManifest.write();\n } catch (error) {\n errors.push(`Persist remote asset manifest: ${formatError(error)}`);\n for (const key of changedManifestKeys) managedKeys.delete(key);\n return { managedKeys, linked: 0, errors };\n }\n }\n\n let linked = 0;\n for (const [key, file] of filesToRemove) {\n try {\n if (file.exists) unlinkSync(file.absolutePath);\n linked++;\n } catch (error) {\n errors.push(`Externalize ${key}: ${formatError(error)}`);\n // The manifest link is safely on disk; retain the resource outside the\n // shadow even if an operating-system lock delays local cleanup.\n }\n }\n\n return { managedKeys, linked, errors };\n }\n\n private isSafeThemeFile(key: string, file: ThemeFile): boolean {\n return (\n !key.includes(\"\\0\") &&\n !key.split(/[\\\\/]/).includes(\"..\") &&\n (file.absolutePath === this.themeRoot.root ||\n file.absolutePath.startsWith(this.themeRoot.root + sep))\n );\n }\n\n private managedAssetNeedsRefresh(\n resource: RemoteResource,\n link: ThemeAssetLink,\n ): boolean {\n if (!isManagedAssetResource(resource)) return true;\n if (link.url !== undefined) return resource.url !== link.url;\n if (link.checksum !== undefined) return resource.checksum !== link.checksum;\n return true;\n }\n\n private ensureManagedAssetsAreResolved(): void {\n const unresolved = this.assetManifest\n .entries()\n .filter(\n ([key]) =>\n !this.themeRoot.ignore.ignore(key) &&\n !this.themeRoot.file(key).exists,\n )\n .map(([key]) => key)\n .filter((key) => !isManagedAssetResource(this.remoteResources.get(key)));\n if (unresolved.length > 0) {\n throw new Error(\n `Managed asset(s) could not be linked: ${unresolved.join(\", \")}`,\n );\n }\n }\n\n // ─── Full Upload ──────────────────────────────────────────────────────────\n\n async uploadTheme(\n opts: {\n delete?: boolean;\n validate?: boolean;\n linkManagedAssets?: { replace?: boolean };\n pendingBinaryAssets?: boolean;\n onProgress?: (done: number, total: number) => void;\n // The SHA `.fluid-theme.json` stored on the last pull. Sent to the\n // server on every PUT/DELETE so a stale local aborts the push\n // instead of clobbering. Null / undefined skips the check entirely\n // (force-push and pre-Phase-003 servers).\n baseSha?: string | null;\n /** The caller already ran the stale-base preflight. */\n skipPreflight?: boolean;\n /** Last acknowledged dev resource index, validated by `baseSha`. */\n remoteState?: DevRemoteState;\n } = {},\n ): Promise<SyncResult> {\n const localFiles = this.themeRoot.files();\n assertNoCaseCollisions(localFiles.map((file) => file.relativePath));\n const result: SyncResult = {\n uploaded: 0,\n deleted: 0,\n downloaded: 0,\n linked: 0,\n errors: [],\n validationFailed: false,\n };\n\n // Schema validation pass\n if (opts.validate) {\n for (const file of localFiles) {\n if (!file.isLiquid) continue;\n const diagnostics = file.validateSchema();\n const errors = diagnostics.filter((d) => d.severity === \"error\");\n for (const d of errors) {\n result.errors.push(`${file.relativePath}: ${d.message}`);\n }\n }\n if (result.errors.length > 0) {\n result.validationFailed = true;\n return result;\n }\n }\n\n if (opts.remoteState) {\n this.useDevRemoteState(opts.remoteState);\n } else {\n await this.fetchChecksums();\n }\n\n // Preflight before any state change so a stale local aborts with\n // zero writes. On 409, `PushConflictError` propagates to the CLI\n // and the per-file loop below never runs.\n if (!opts.skipPreflight) await this.preflightPush(opts.baseSha);\n\n // Roll the base_sha forward as the server bumps its version on\n // each successful write. Start from the caller's stored SHA (from\n // pull) and update from every response — the next PUT carries the\n // freshest SHA the server has given us so a concurrent third-party\n // write mid-loop 409s cleanly instead of the CLI overwriting.\n let baseSha = opts.baseSha ?? null;\n\n if (opts.linkManagedAssets) {\n result.linked = await this.linkManagedAssets(opts.linkManagedAssets);\n // FileResource writes can change the server's content version. The list\n // after reference creation gives the next normal resource write a fresh\n // base when the server supports Phase 003a.\n baseSha = this.lastKnownRemoteSha ?? baseSha;\n } else if (this.assetManifestInstance) {\n this.assetManifest.reload();\n this.ensureManagedAssetsAreResolved();\n }\n\n const toUpload = localFiles.filter((f) => f.exists && this.hasChanged(f));\n let done = 0;\n for (const file of toUpload) {\n try {\n await this.uploadFile(file, baseSha, {\n pendingAsset: opts.pendingBinaryAssets,\n });\n // `putResource` updates `lastKnownRemoteSha` from the server's\n // response. Thread it forward so the next iteration carries\n // the current SHA — this is how mid-push races surface.\n baseSha = this.lastKnownRemoteSha;\n result.uploaded++;\n } catch (e) {\n if (e instanceof PushConflictError) throw e;\n result.errors.push(`Upload ${file.relativePath}: ${formatError(e)}`);\n }\n opts.onProgress?.(++done, toUpload.length);\n }\n\n if (opts.delete) {\n const localPaths = new Set(localFiles.map((f) => f.relativePath));\n for (const key of this.assetManifest.keys()) localPaths.add(key);\n const toDelete = this.remoteKeys().filter(\n (key) =>\n this.canDeleteRemoteResource(key) &&\n !localPaths.has(key) &&\n !this.themeRoot.ignore.ignore(key),\n );\n for (const key of toDelete) {\n try {\n await this.deleteRemoteFile(key, baseSha);\n baseSha = this.lastKnownRemoteSha;\n result.deleted++;\n } catch (e) {\n if (e instanceof PushConflictError) throw e;\n result.errors.push(`Delete ${key}: ${formatError(e)}`);\n }\n }\n }\n\n return result;\n }\n\n // ─── Full Download ────────────────────────────────────────────────────────\n\n async downloadTheme(\n opts: {\n delete?: boolean;\n skip?: Set<string>;\n onProgress?: (done: number, total: number) => void;\n } = {},\n ): Promise<SyncResult & { skipped: number }> {\n const resources = await this.downloadAll();\n const externalizedAssets = await this.externalizePulledAssets(resources, {\n delete: opts.delete ?? false,\n });\n const result: SyncResult & { skipped: number } = {\n uploaded: 0,\n deleted: 0,\n downloaded: 0,\n linked: externalizedAssets.linked,\n skipped: 0,\n errors: [...externalizedAssets.errors],\n validationFailed: false,\n };\n\n let done = 0;\n for (const resource of resources) {\n if (externalizedAssets.managedKeys.has(resource.key)) {\n opts.onProgress?.(++done, resources.length);\n continue;\n }\n if (opts.skip?.has(resource.key)) {\n result.skipped++;\n opts.onProgress?.(++done, resources.length);\n continue;\n }\n\n const file = this.themeRoot.file(resource.key);\n\n // Guard against path traversal from malicious API responses\n if (!this.isSafeThemeFile(resource.key, file)) {\n result.errors.push(`Download ${resource.key}: path traversal detected`);\n opts.onProgress?.(++done, resources.length);\n continue;\n }\n\n try {\n if (resource.resource_type === \"FileResource\" && resource.url) {\n const buf = await this.downloadBinaryAsset(resource.url);\n file.write(buf);\n } else if (\n resource.content !== undefined &&\n resource.content !== null\n ) {\n const content =\n typeof resource.content === \"string\"\n ? resource.content\n : JSON.stringify(resource.content);\n file.write(content);\n }\n result.downloaded++;\n } catch (e) {\n result.errors.push(`Download ${resource.key}: ${formatError(e)}`);\n }\n opts.onProgress?.(++done, resources.length);\n }\n\n if (opts.delete) {\n const remoteKeys = new Set(resources.map((r) => r.key));\n for (const file of this.themeRoot.files()) {\n if (remoteKeys.has(file.relativePath)) continue;\n // Preserve local stylesheets that the backend hides under\n // STYLESHEET_STRICT_INPUT — their absence from the resources\n // index is intentional (theme-level styles.css /\n // global_styles.css and per-template composite\n // {type}/{name}/styles.css), not a signal that the merchant\n // deleted them.\n if (isStylesheetKey(file.relativePath)) continue;\n\n try {\n unlinkSync(file.absolutePath);\n result.deleted++;\n } catch {\n // ignore\n }\n }\n }\n\n return result;\n }\n}\n\nfunction isLinkableBinaryResource(\n resource: RemoteResource,\n key: string,\n file: ThemeFile,\n): boolean {\n return (\n isThemeAssetKey(key) && isManagedAssetResource(resource) && !file.isText\n );\n}\n\nfunction isNestedBinaryThemeAsset(file: ThemeFile): boolean {\n return (\n !file.isText &&\n file.relativePath.startsWith(\"assets/\") &&\n !isThemeAssetKey(file.relativePath)\n );\n}\n\nfunction isManagedAssetResource(\n resource: RemoteResource | undefined,\n): resource is RemoteResource {\n return (\n resource?.resource_type === \"FileResource\" &&\n typeof resource.url === \"string\" &&\n resource.url.length > 0\n );\n}\n\nfunction remoteResourceFromState(state: RemoteResourceState): RemoteResource {\n return {\n key: state.key,\n checksum: state.checksum,\n content: state.contentPresent ? \"\" : null,\n resource_type: state.resourceType,\n resource_id: state.resourceId,\n url: state.url,\n };\n}\n\nfunction remoteResourceState(resource: RemoteResource): RemoteResourceState {\n return {\n key: resource.key,\n checksum: resource.checksum,\n contentPresent: resource.content != null,\n resourceType: resource.resource_type,\n resourceId: resource.resource_id,\n url: resource.url,\n };\n}\n\nfunction isNotFoundError(error: unknown): boolean {\n return isApiError(error) && error.status === 404;\n}\n\nfunction resourceLink(\n resource: RemoteResource,\n): Pick<ThemeAssetLink, \"checksum\" | \"url\"> {\n return {\n ...(typeof resource.checksum === \"string\" && resource.checksum.length > 0\n ? { checksum: resource.checksum }\n : {}),\n ...(typeof resource.url === \"string\" && resource.url.length > 0\n ? { url: resource.url }\n : {}),\n };\n}\n\nfunction assetFilename(key: string): string {\n return key.slice(\"assets/\".length);\n}\n\nfunction assetMetadataFromLink(\n link: ThemeAssetLink,\n): RemoteAssetMetadata | undefined {\n if (\n typeof link.url !== \"string\" ||\n link.url.length === 0 ||\n typeof link.contentType !== \"string\" ||\n link.contentType.length === 0 ||\n typeof link.contentSize !== \"number\" ||\n !Number.isInteger(link.contentSize) ||\n link.contentSize <= 0\n ) {\n return undefined;\n }\n\n return {\n url: link.url,\n contentType: link.contentType,\n contentSize: link.contentSize,\n ...(typeof link.previewImageUrl === \"string\"\n ? { previewImageUrl: link.previewImageUrl }\n : {}),\n ...(typeof link.altText === \"string\" ? { altText: link.altText } : {}),\n ...(typeof link.handle === \"string\" ? { handle: link.handle } : {}),\n };\n}\n\nfunction uploadedAssetMetadata(\n file: ThemeFile,\n resource: UploadedBinaryResource,\n): RemoteAssetMetadata {\n if (resource.assetMetadata) return resource.assetMetadata;\n if (!resource.url) {\n throw new Error(`Uploaded asset has no URL: ${file.relativePath}`);\n }\n\n return {\n url: resource.url,\n contentType: file.mime.name,\n contentSize: file.size(),\n altText: file.name,\n ...(typeof resource.damAssetCode === \"string\"\n ? { handle: resource.damAssetCode }\n : {}),\n };\n}\n\nfunction parseThemeAssetMetadata(\n value: unknown,\n): { filename: string; metadata: RemoteAssetMetadata } | undefined {\n if (!isRecord(value)) return undefined;\n\n const filename = nonEmptyString(value[\"filename\"]);\n const url = nonEmptyString(value[\"url\"]);\n const contentType = nonEmptyString(value[\"content_type\"]);\n const contentSize = positiveInteger(value[\"content_size\"]);\n if (!filename || !url || !contentType || !contentSize) return undefined;\n\n const previewImageUrl = nonEmptyString(value[\"preview_image_url\"]);\n const altText = optionalString(value[\"alt_text\"]);\n const handle = nonEmptyString(value[\"handle\"]);\n return {\n filename,\n metadata: {\n url,\n contentType,\n contentSize,\n ...(previewImageUrl ? { previewImageUrl } : {}),\n ...(altText !== undefined ? { altText } : {}),\n ...(handle ? { handle } : {}),\n },\n };\n}\n\nfunction createdFileResourceId(value: unknown): number | undefined {\n if (!isRecord(value) || !isRecord(value[\"file_resource\"])) {\n return undefined;\n }\n return positiveInteger(value[\"file_resource\"][\"id\"]);\n}\n\nfunction positiveInteger(value: unknown): number | undefined {\n if (typeof value === \"number\" && Number.isInteger(value) && value > 0) {\n return value;\n }\n if (typeof value !== \"string\" || !/^\\d+$/.test(value)) return undefined;\n\n const parsed = Number(value);\n return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n\nfunction nonEmptyString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\nfunction optionalString(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n","/**\n * Heuristic-only check for obviously unbalanced liquid delimiters\n * (`{% %}` and `{{ }}`). This is NOT a liquid parser — it only tracks\n * opening delimiters until they are closed. It exists to give watch-mode users a signal\n * when a save is liquid-syntax-broken: the server accepts\n * syntax-broken liquid silently on upload, and the storefront\n * renderer then serves stale content for that section with no error\n * anywhere else in the pipeline.\n *\n * Closing-looking tokens without a preceding Liquid opener are intentionally\n * ignored. Liquid files commonly contain CSS such as `width:100%}` or adjacent\n * block braces (`}}`), so treating every close token as Liquid creates noisy\n * false warnings on valid theme files.\n *\n * Known false positive: delimiters written literally inside a\n * `{% raw %}...{% endraw %}` block are still counted and can trip\n * this check even though the liquid is valid. Acceptable for a\n * warn-only heuristic — a real fix requires parsing liquid, which is\n * out of scope here (see the server-side validation note in the PR).\n */\nexport function hasUnbalancedLiquidDelimiters(content: string): boolean {\n let unclosedTags = 0;\n let unclosedOutputs = 0;\n\n for (const token of content.matchAll(/\\{%|%\\}|\\{\\{|\\}\\}/g)) {\n switch (token[0]) {\n case \"{%\":\n unclosedTags += 1;\n break;\n case \"%}\":\n if (unclosedTags > 0) unclosedTags -= 1;\n break;\n case \"{{\":\n unclosedOutputs += 1;\n break;\n case \"}}\":\n if (unclosedOutputs > 0) unclosedOutputs -= 1;\n break;\n }\n }\n\n return unclosedTags > 0 || unclosedOutputs > 0;\n}\n\nexport interface LiquidBlockTagDiagnostic {\n severity: \"error\";\n message: string;\n}\n\nconst BLOCK_TAGS = new Map([\n [\"capture\", \"endcapture\"],\n [\"case\", \"endcase\"],\n [\"comment\", \"endcomment\"],\n [\"for\", \"endfor\"],\n [\"form\", \"endform\"],\n [\"if\", \"endif\"],\n [\"ifchanged\", \"endifchanged\"],\n [\"javascript\", \"endjavascript\"],\n [\"paginate\", \"endpaginate\"],\n [\"raw\", \"endraw\"],\n [\"schema\", \"endschema\"],\n [\"style\", \"endstyle\"],\n [\"stylesheet\", \"endstylesheet\"],\n [\"tablerow\", \"endtablerow\"],\n [\"unless\", \"endunless\"],\n]);\n\nconst CLOSING_TAGS = new Set(BLOCK_TAGS.values());\nconst OPAQUE_BLOCK_TAGS = new Set([\n \"comment\",\n \"javascript\",\n \"raw\",\n \"schema\",\n \"style\",\n \"stylesheet\",\n]);\n\ninterface OpenBlock {\n name: string;\n expectedClose: string;\n line: number;\n}\n\n/**\n * Find structurally unbalanced Liquid block tags such as an `{% if %}` with\n * no `{% endif %}`. This intentionally recognizes only established paired\n * tags; custom and inline tags are ignored rather than guessed at.\n *\n * Content inside raw/comment/schema/style/javascript blocks is opaque to\n * Liquid and therefore skipped until that block's matching close tag. This\n * prevents CSS, JSON, and examples containing Liquid-looking text from\n * producing false errors.\n */\nexport function findLiquidBlockTagDiagnostics(\n content: string,\n): LiquidBlockTagDiagnostic[] {\n const stack: OpenBlock[] = [];\n const diagnostics: LiquidBlockTagDiagnostic[] = [];\n let line = 1;\n let previousTagIndex = 0;\n\n const processTag = (name: string, tagLine: number): void => {\n const open = stack.at(-1);\n\n if (open && OPAQUE_BLOCK_TAGS.has(open.name)) {\n if (name === open.expectedClose) stack.pop();\n return;\n }\n\n const expectedClose = BLOCK_TAGS.get(name);\n if (expectedClose) {\n stack.push({ name, expectedClose, line: tagLine });\n return;\n }\n\n if (!CLOSING_TAGS.has(name)) return;\n\n if (!open) {\n diagnostics.push({\n severity: \"error\",\n message: `Unexpected Liquid tag '{% ${name} %}' on line ${tagLine}; there is no open block to close.`,\n });\n return;\n }\n\n if (name !== open.expectedClose) {\n diagnostics.push({\n severity: \"error\",\n message: `Mismatched Liquid tag '{% ${name} %}' on line ${tagLine}; '{% ${open.name} %}' from line ${open.line} must close with '{% ${open.expectedClose} %}'.`,\n });\n return;\n }\n\n stack.pop();\n };\n\n for (const match of content.matchAll(\n /\\{%-?\\s*([a-zA-Z_][\\w-]*)\\b(?:(?!\\{%)[\\s\\S])*?-?%\\}/g,\n )) {\n const name = match[1]?.toLowerCase();\n if (!name) continue;\n\n const index = match.index ?? 0;\n // Count each character at most once across the scan. Re-slicing from the\n // start for every tag turns large generated templates into O(n²) work.\n for (let cursor = previousTagIndex; cursor < index; cursor++) {\n if (content.charCodeAt(cursor) === 10) line += 1;\n }\n previousTagIndex = index;\n\n const open = stack.at(-1);\n if (name === \"liquid\" && !(open && OPAQUE_BLOCK_TAGS.has(open.name))) {\n // `{% liquid %}` places one delimiter-free statement on each line. Feed\n // those statements through the same stack so this supported syntax\n // cannot bypass lint, push, or watch-mode validation.\n const statements = match[0]\n .replace(/^\\{%-?\\s*liquid\\b/i, \"\")\n .replace(/-?%\\}$/, \"\")\n .split(\"\\n\");\n for (const [offset, statement] of statements.entries()) {\n const statementName = /^\\s*([a-zA-Z_][\\w-]*)\\b/.exec(statement)?.[1];\n if (!statementName) continue;\n processTag(statementName.toLowerCase(), line + offset);\n }\n } else {\n processTag(name, line);\n }\n }\n\n for (const open of stack.reverse()) {\n diagnostics.push({\n severity: \"error\",\n message: `Unclosed Liquid tag '{% ${open.name} %}' on line ${open.line}; expected '{% ${open.expectedClose} %}'.`,\n });\n }\n\n return diagnostics;\n}\n","import net from \"node:net\";\n\n/**\n * The dev command does real work before it ever binds a port: it resolves\n * (or creates) a server-side dev theme and runs a full initial sync, which\n * can take minutes on a large theme. If the requested port is already taken\n * — most commonly by another `fluid theme dev` or the Mist Desktop preview,\n * which both default to 9292 — all of that work is wasted and the process\n * used to die with a raw `EADDRINUSE` stack trace. Call this before any of\n * that work starts so we fail fast with a clear message instead.\n */\nexport class PortInUseError extends Error {\n constructor(\n public readonly host: string,\n public readonly port: number,\n ) {\n super(formatPortConflictMessage(host, port));\n this.name = \"PortInUseError\";\n }\n}\n\nexport function formatPortConflictMessage(host: string, port: number): string {\n return (\n `Port ${port} on ${host} is already in use — likely another ` +\n \"`fluid theme dev` or the Mist Desktop preview. Stop the other \" +\n \"server or pass --port <number>.\"\n );\n}\n\n/**\n * Attempt to bind `host:port`, then immediately release it. Resolves if the\n * port is free; rejects with `PortInUseError` on `EADDRINUSE`/`EACCES`, or\n * the raw error for anything else unexpected.\n */\nexport function checkPortAvailable(host: string, port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n const server = net.createServer();\n\n server.once(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\" || err.code === \"EACCES\") {\n reject(new PortInUseError(host, port));\n } else {\n reject(err);\n }\n });\n\n server.once(\"listening\", () => {\n server.close(() => resolve());\n });\n\n server.listen(port, host);\n });\n}\n","import http from \"node:http\";\nimport { SSEStream } from \"./sse.js\";\nimport { proxyRequest } from \"./proxy.js\";\nimport { watchTheme } from \"./watcher.js\";\nimport { PushConflictError, Syncer } from \"../syncer.js\";\nimport {\n findLiquidBlockTagDiagnostics,\n hasUnbalancedLiquidDelimiters,\n} from \"../liquid-delimiters.js\";\nimport type { ThemeRoot } from \"../root.js\";\nimport type { ApiClient } from \"../../api.js\";\nimport { formatPortConflictMessage } from \"./port-preflight.js\";\nimport { assertNoCaseCollisions } from \"../case-collisions.js\";\nimport { isStylesheetKey } from \"../stylesheet-keys.js\";\nimport { ThemeAssetManifest } from \"../asset-manifest.js\";\nimport type { DevRemoteState } from \"../dev-remote-baseline.js\";\n\nfunction timestamp(): string {\n return new Date().toLocaleTimeString(\"en-US\", { hour12: false });\n}\n\nexport interface DevServerOptions {\n host: string;\n port: number;\n reloadMode: \"full-page\" | \"off\";\n /** Cached dev resource index paired with its acknowledged remote version. */\n initialSync?: DevRemoteState;\n /** Persist the complete dev resource baseline and its version atomically. */\n onRemoteState?: (state: DevRemoteState) => void;\n /** Discard persisted trust after the server rejects a watched write. */\n onRemoteStateInvalidated?: () => void;\n}\n\nexport interface DevServerTheme {\n id: number;\n name: string;\n company: string;\n editorUrl?: string;\n}\n\nexport async function startDevServer(\n api: ApiClient,\n theme: DevServerTheme,\n themeRoot: ThemeRoot,\n opts: DevServerOptions & { validate?: boolean },\n onReady?: (address: string) => void,\n): Promise<() => void> {\n const sse = new SSEStream();\n const syncer = new Syncer(api, theme.id, themeRoot);\n let remoteStateSafe = true;\n const invalidateRemoteState = () => {\n if (!remoteStateSafe) return;\n remoteStateSafe = false;\n try {\n opts.onRemoteStateInvalidated?.();\n } catch {\n // The stale version will fail preflight on the next startup.\n }\n };\n const recordRemoteState = () => {\n if (!remoteStateSafe) return;\n if (opts.onRemoteState) {\n const state = syncer.devRemoteState(\n new ThemeAssetManifest(themeRoot.root).fingerprint(),\n );\n if (state) {\n try {\n opts.onRemoteState(state);\n } catch {\n invalidateRemoteState();\n }\n }\n }\n };\n\n const pendingUpdates = new Set<string>();\n\n // ── Initial sync ─────────────────────────────────────────────────────────\n console.log(`\\nSyncing theme ${theme.name} (#${theme.id})…`);\n const progress = (done: number, total: number) => {\n process.stdout.write(`\\r Uploading ${done}/${total} files…`);\n };\n const uploadFromRemoteIndex = () =>\n syncer.uploadTheme({\n delete: true,\n validate: opts.validate,\n linkManagedAssets: { replace: true },\n pendingBinaryAssets: true,\n onProgress: progress,\n });\n let syncResult;\n if (opts.initialSync) {\n let remoteStateIsTrusted = false;\n try {\n syncer.useDevRemoteState(opts.initialSync);\n await syncer.preflightPush(opts.initialSync.remoteSha);\n remoteStateIsTrusted = true;\n } catch (error) {\n if (!(error instanceof PushConflictError)) throw error;\n }\n\n syncResult = remoteStateIsTrusted\n ? await syncer.uploadTheme({\n delete: true,\n validate: opts.validate,\n linkManagedAssets: { replace: true },\n pendingBinaryAssets: true,\n baseSha: syncer.remoteSha(),\n remoteState: opts.initialSync,\n skipPreflight: true,\n onProgress: progress,\n })\n : await uploadFromRemoteIndex();\n } else {\n syncResult = await uploadFromRemoteIndex();\n }\n process.stdout.write(\"\\n\");\n if (syncResult.linked > 0) {\n console.log(` Saved ${syncResult.linked} remote asset reference(s).`);\n }\n if (syncResult.validationFailed) {\n console.error(\n `\\nSchema validation failed (${syncResult.errors.length} error(s)). Use --force to skip.\\n`,\n );\n for (const e of syncResult.errors) console.error(` ${e}`);\n process.exit(1);\n } else if (syncResult.errors.length > 0) {\n invalidateRemoteState();\n for (const e of syncResult.errors) console.error(` ${e}`);\n if (syncResult.uploaded + syncResult.deleted === 0) process.exit(1);\n }\n if (syncResult.errors.length === 0) {\n recordRemoteState();\n }\n\n // ── File watcher ─────────────────────────────────────────────────────────\n //\n // Uploading a file no longer commits anything — Fluid only records that the\n // theme changed, and something has to ask for the commit. `theme push` asks\n // once when its loop ends; the watcher has no such moment, since chokidar\n // reports one event per file and nothing in it says whether this is a lone\n // save or the 200th write of a restore.\n //\n // So ask once the writes stop — but decide where \"the writes\" end by when\n // events ARRIVED, not by when their handlers ran. Handlers are serialized\n // behind awaited uploads, so a genuinely separate edit can queue behind a\n // slow one and start immediately after it; timing the handlers would fold\n // the two into a single commit and erase the boundary between them.\n //\n // An event that arrived within the window continues the operation already\n // in flight, so its pending ask is cancelled and rescheduled. One that\n // arrived after a real pause ends that operation, so the ask goes out NOW\n // rather than being cancelled — otherwise the earlier edit would be\n // swallowed into this one's commit.\n //\n // Asks are also chained against the uploads. A fire-and-forget request can\n // overlap the next batch, committing a half-written operation or leaving\n // the rest of it pending; awaiting the outstanding one before uploading\n // keeps each commit to a settled state.\n //\n // If the server stops with an ask still pending, Fluid's sweeper picks the\n // change up; the cost is a delay, never a lost commit.\n const SYNC_IDLE_MS = 2_000;\n let lastArrivedAt = 0;\n let pendingSync: ReturnType<typeof setTimeout> | null = null;\n let syncInFlight: Promise<void> = Promise.resolve();\n\n // Tracks an ask Fluid did not take. Until it lands the previous edit is\n // uncommitted, and uploading the next one over it merges the two into\n // whichever commit eventually arrives — so the boundary is retried before\n // any further writes go out. A retry that also fails has done what a client\n // can: the content is safe on the server and the sweeper commits it, but\n // the two edits will share a version.\n let askOwed = false;\n let remoteWritesBlocked = false;\n const blockRemoteWrites = (error: PushConflictError): void => {\n if (remoteWritesBlocked) return;\n remoteWritesBlocked = true;\n invalidateRemoteState();\n console.error(\n `\\n[Watcher] Remote theme changed outside this dev session (${error.message}). Restart theme dev to compare the current remote state before writing again.`,\n );\n };\n const sendSync = (): void => {\n syncInFlight = syncInFlight.then(async () => {\n const accepted = await syncer.requestSync();\n askOwed = !accepted;\n if (accepted) recordRemoteState();\n });\n };\n const flushSyncNow = (): void => {\n if (!pendingSync) return;\n clearTimeout(pendingSync);\n pendingSync = null;\n sendSync();\n };\n const scheduleSync = (): void => {\n if (pendingSync) clearTimeout(pendingSync);\n pendingSync = setTimeout(() => {\n pendingSync = null;\n sendSync();\n }, SYNC_IDLE_MS);\n };\n\n const stopWatcher = watchTheme(\n themeRoot,\n async (modified, added, removed, arrivedAt) => {\n if (arrivedAt - lastArrivedAt > SYNC_IDLE_MS) {\n // A real pause: whatever was pending belongs to the previous edit.\n flushSyncNow();\n } else if (pendingSync) {\n // Same operation still arriving — hold the ask.\n clearTimeout(pendingSync);\n pendingSync = null;\n }\n lastArrivedAt = arrivedAt;\n // Never upload across an outstanding ask, or it commits a half-written\n // operation.\n await syncInFlight;\n // An ask Fluid refused still owes the edit before this one its own\n // commit. Retry before writing over it.\n if (askOwed) {\n sendSync();\n await syncInFlight;\n }\n if (remoteWritesBlocked) return;\n\n try {\n assertNoCaseCollisions(\n themeRoot.files().map((file) => file.relativePath),\n );\n } catch (error) {\n console.error(`\\n[Watcher] Sync blocked: ${String(error)}`);\n return;\n }\n\n const changed = [...modified, ...added];\n let wroteRemote = false;\n\n for (const file of changed) {\n // Validate schema on liquid files during dev (warn, don't block)\n if (opts.validate && file.isLiquid) {\n const diagnostics = file.validateSchema();\n for (const d of diagnostics) {\n const prefix =\n d.severity === \"error\" ? \"Schema error\" : \"Schema warning\";\n console.warn(`\\n[${prefix}] ${file.relativePath}: ${d.message}`);\n }\n }\n\n pendingUpdates.add(file.relativePath);\n try {\n // uploadFile() resolves with the exact bytes it sent, so the\n // diagnostic below always describes the uploaded content —\n // never a re-read of a file an editor may have changed,\n // replaced, or deleted while the request was in flight.\n const uploadedContent = await syncer.uploadFile(\n file,\n syncer.remoteSha(),\n { pendingAsset: true },\n );\n wroteRemote = true;\n console.log(` ✓ synced ${file.relativePath} (${timestamp()})`);\n // \"synced\" is true of the upload and false of the page. The\n // storefront inlines a stylesheet from a Themes::FileResource that a\n // background job rewrites after the publish commits, so\n // `inline_asset_content` keeps emitting the previous bytes for a\n // moment after the server accepts this write. Say so, or the ✓ reads\n // as \"the rendered CSS is up to date\" (CURRENT-3899).\n if (isStylesheetKey(file.relativePath)) {\n console.warn(\n ` ⚠ ${file.relativePath}: the storefront inlines this CSS from a published asset, so it serves the previous bytes until the stylesheet asset sync finishes`,\n );\n }\n // Cheap warn-only heuristic: the server accepts syntax-broken\n // liquid silently, and the storefront then serves stale\n // content for the section with no error anywhere. This does\n // not parse liquid — it can false-positive on delimiters\n // written literally inside {% raw %} — but it's the only\n // client-side signal a save is likely broken.\n if (\n file.isLiquid &&\n uploadedContent !== null &&\n hasUnbalancedLiquidDelimiters(uploadedContent)\n ) {\n console.warn(\n ` ⚠ ${file.relativePath}: unbalanced liquid delimiters — the storefront may silently serve stale content for this section`,\n );\n }\n if (file.isLiquid && uploadedContent !== null) {\n for (const diagnostic of findLiquidBlockTagDiagnostics(\n uploadedContent,\n )) {\n console.warn(` ⚠ ${file.relativePath}: ${diagnostic.message}`);\n }\n }\n } catch (e) {\n if (e instanceof PushConflictError) {\n blockRemoteWrites(e);\n break;\n }\n invalidateRemoteState();\n console.error(\n `\\n[Watcher] Upload failed: ${file.relativePath}: ${e}`,\n );\n } finally {\n pendingUpdates.delete(file.relativePath);\n }\n }\n\n if (remoteWritesBlocked) return;\n\n for (const file of removed) {\n if (themeRoot.ignore.ignore(file.relativePath)) continue;\n try {\n await syncer.deleteRemoteFile(file.relativePath, syncer.remoteSha());\n wroteRemote = true;\n console.log(` ✓ removed ${file.relativePath}`);\n } catch (error) {\n if (error instanceof PushConflictError) {\n blockRemoteWrites(error);\n break;\n }\n invalidateRemoteState();\n }\n }\n\n if (remoteWritesBlocked) return;\n\n if (wroteRemote) recordRemoteState();\n\n if (removed.length > 0) {\n sse.broadcast(JSON.stringify({ reload_page: true }));\n } else if (changed.length > 0) {\n sse.broadcast(\n JSON.stringify({ modified: changed.map((f) => f.relativePath) }),\n );\n }\n\n scheduleSync();\n },\n );\n\n // ── HTTP server ───────────────────────────────────────────────────────────\n const server = http.createServer(async (req, res) => {\n if (req.url === \"/hot-reload\") {\n sse.add(res);\n return;\n }\n\n try {\n await proxyRequest(req, res, {\n company: theme.company,\n themeId: theme.id,\n reloadMode: opts.reloadMode,\n pendingFiles: () =>\n [...pendingUpdates]\n .map((p) => themeRoot.file(p))\n .filter((f) => f.isText)\n .map((f) => ({\n relativePath: f.relativePath,\n read: () => f.read(),\n })),\n });\n } catch (e) {\n console.error(`[Proxy] ${req.method} ${req.url} → ${e}`);\n if (!res.headersSent) {\n // Surface the real upstream failure instead of a bare 502.\n // Every local render round-trips through <company>.fluid.app,\n // so this error is usually environmental — a TLS-inspecting\n // security agent whose root CA the browser trusts via the OS\n // keychain but Node does not, DNS, or a dropped connection —\n // and a naked \"Bad Gateway\" sends humans and QA agents off\n // chasing the theme instead of the network. Diagnosed live:\n // four workflow page steps went needs-review over one masked\n // cert error.\n const message = e instanceof Error ? e.message : String(e);\n res.writeHead(502, { \"content-type\": \"text/plain; charset=utf-8\" });\n res.end(\n `Bad Gateway — the local preview could not reach ${theme.company}.fluid.app: ${message}\\n` +\n \"This is the dev machine's network path to Fluid, not the theme. \" +\n \"Common causes: TLS-inspecting security software (its root CA is in the OS keychain, which Node does not read — set NODE_EXTRA_CA_CERTS to its certificate), DNS, or a proxy. \" +\n \"The same error is logged by the theme dev server process.\",\n );\n }\n }\n });\n\n // The preflight check in `dev.ts` runs before the (potentially minutes-long)\n // initial sync above, but the port could still be grabbed by another\n // process in the window between that check and this `listen()` call.\n // Handle that race the same way: a friendly message and a clean exit\n // instead of an unhandled `EADDRINUSE`/`EACCES` stack trace.\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\" || err.code === \"EACCES\") {\n console.error(formatPortConflictMessage(opts.host, opts.port));\n process.exit(1);\n }\n reject(err);\n });\n server.listen(opts.port, opts.host, () => resolve());\n });\n\n const address = `http://${opts.host}:${opts.port}`;\n onReady?.(address);\n\n // ── Teardown ──────────────────────────────────────────────────────────────\n return function stop() {\n sse.close();\n stopWatcher();\n server.close();\n };\n}\n","import { createHash, randomBytes } from \"node:crypto\";\nimport {\n mkdirSync,\n readFileSync,\n renameSync,\n rmSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { assertNoCaseCollisions } from \"./case-collisions.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport type { ShadowRepo } from \"./shadow-repo.js\";\nimport {\n MANAGED_ASSET_SHADOW_SENTINEL,\n ThemeAssetManifest,\n} from \"./asset-manifest.js\";\n\nconst BASELINE_VERSION = 1;\nconst BASELINE_FILE = join(\".fluid-theme\", \"dev-baseline.json\");\n\nexport interface RemoteResourceState {\n key: string;\n checksum: string | null;\n contentPresent: boolean;\n resourceType?: string | null;\n resourceId?: number | null;\n url?: string | null;\n}\n\nexport interface DevRemoteState {\n themeId: number;\n remoteSha: string;\n assetManifestSha: string;\n resources: RemoteResourceState[];\n}\n\nexport function readDevRemoteBaseline(\n themeRoot: string,\n themeId: number,\n assetManifestSha: string,\n): DevRemoteState | null {\n try {\n const parsed = JSON.parse(\n readFileSync(join(themeRoot, BASELINE_FILE), \"utf-8\"),\n );\n const state = parseDocument(parsed);\n if (state.themeId !== themeId) return null;\n if (state.assetManifestSha !== assetManifestSha) return null;\n return state;\n } catch {\n return null;\n }\n}\n\nexport function writeDevRemoteBaseline(\n themeRoot: string,\n state: DevRemoteState,\n): void {\n const document = { version: BASELINE_VERSION, ...state };\n parseDocument(document);\n\n const path = join(themeRoot, BASELINE_FILE);\n const tempPath = `${path}.${randomBytes(6).toString(\"hex\")}.tmp`;\n try {\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(tempPath, `${JSON.stringify(document)}\\n`, {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n renameSync(tempPath, path);\n } catch (error) {\n rmSync(tempPath, { force: true });\n throw error;\n }\n}\n\nexport function removeDevRemoteBaseline(themeRoot: string): void {\n rmSync(join(themeRoot, BASELINE_FILE), { force: true });\n}\n\nexport async function devRemoteStateFromSourceShadow(\n themeRoot: ThemeRoot,\n shadow: ShadowRepo,\n themeId: number,\n remoteSha: string,\n): Promise<DevRemoteState | null> {\n try {\n if (!(await shadow.hasHead())) return null;\n\n const assetManifest = new ThemeAssetManifest(themeRoot.root);\n const resources: RemoteResourceState[] = [];\n for (const key of await shadow.headPaths()) {\n const content = await shadow.blobAtHead(key);\n if (!content) return null;\n const asset = assetManifest.get(key);\n if (asset) {\n if (\n asset.pending ||\n !asset.url ||\n !content.equals(Buffer.from(MANAGED_ASSET_SHADOW_SENTINEL))\n ) {\n return null;\n }\n resources.push({\n key,\n checksum: asset.checksum ?? null,\n contentPresent: false,\n resourceType: \"FileResource\",\n url: asset.url,\n });\n continue;\n }\n\n if (content.equals(Buffer.from(MANAGED_ASSET_SHADOW_SENTINEL)))\n return null;\n resources.push({\n key,\n checksum: createHash(\"sha256\").update(content).digest(\"hex\"),\n contentPresent: true,\n });\n }\n\n return {\n themeId,\n remoteSha,\n assetManifestSha: assetManifest.fingerprint(),\n resources,\n };\n } catch {\n return null;\n }\n}\n\nfunction parseDocument(document: unknown): DevRemoteState {\n if (!isRecord(document) || document[\"version\"] !== BASELINE_VERSION) {\n throw new Error(`expected version ${BASELINE_VERSION}`);\n }\n\n const themeId = document[\"themeId\"];\n const remoteSha = document[\"remoteSha\"];\n const assetManifestSha = document[\"assetManifestSha\"];\n const rawResources = document[\"resources\"];\n if (!isPositiveInteger(themeId)) throw new Error(\"invalid theme id\");\n if (!isNonEmptyString(remoteSha)) throw new Error(\"invalid remote sha\");\n if (!isNonEmptyString(assetManifestSha)) {\n throw new Error(\"invalid asset manifest sha\");\n }\n if (!Array.isArray(rawResources)) throw new Error(\"invalid resources\");\n\n const resources = rawResources.map(parseResource);\n assertNoCaseCollisions(resources.map((resource) => resource.key));\n return { themeId, remoteSha, assetManifestSha, resources };\n}\n\nfunction parseResource(resource: unknown): RemoteResourceState {\n if (!isRecord(resource)) throw new Error(\"invalid resource\");\n\n const key = resource[\"key\"];\n const checksum = resource[\"checksum\"];\n const contentPresent = resource[\"contentPresent\"];\n const resourceType = resource[\"resourceType\"];\n const resourceId = resource[\"resourceId\"];\n const url = resource[\"url\"];\n if (!isNonEmptyString(key) || key.includes(\"\\0\")) {\n throw new Error(\"invalid resource key\");\n }\n if (checksum !== null && !isNonEmptyString(checksum)) {\n throw new Error(\"invalid resource checksum\");\n }\n if (typeof contentPresent !== \"boolean\") {\n throw new Error(\"invalid resource content marker\");\n }\n if (\n resourceType !== undefined &&\n resourceType !== null &&\n !isNonEmptyString(resourceType)\n ) {\n throw new Error(\"invalid resource type\");\n }\n if (\n resourceId !== undefined &&\n resourceId !== null &&\n !isPositiveInteger(resourceId)\n ) {\n throw new Error(\"invalid resource id\");\n }\n if (url !== undefined && url !== null && !isNonEmptyString(url)) {\n throw new Error(\"invalid resource url\");\n }\n\n return {\n key,\n checksum,\n contentPresent,\n resourceType,\n resourceId,\n url,\n };\n}\n\nfunction isRecord(candidate: unknown): candidate is Record<string, unknown> {\n return typeof candidate === \"object\" && candidate !== null;\n}\n\nfunction isNonEmptyString(candidate: unknown): candidate is string {\n return typeof candidate === \"string\" && candidate.length > 0;\n}\n\nfunction isPositiveInteger(candidate: unknown): candidate is number {\n return (\n typeof candidate === \"number\" &&\n Number.isInteger(candidate) &&\n candidate > 0\n );\n}\n","import { spawn } from \"node:child_process\";\nimport {\n existsSync,\n mkdirSync,\n mkdtempSync,\n readFileSync,\n rmSync,\n writeFileSync,\n} from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/**\n * A bare git repo hidden under `.fluid-theme/repo` that stands in for\n * git's index+HEAD when talking to the Fluid server. Every pull commits\n * the incoming remote state onto a single branch (`refs/heads/main`);\n * every successful push commits the outgoing local state onto the same\n * branch. HEAD therefore represents \"the last state the CLI and server\n * agreed on\", and its tree is the natural three-way-merge base for the\n * next pull.\n *\n * We stay in git's plumbing layer — no working tree, no index file\n * next to the theme content — so the shadow repo cannot interfere\n * with the user's own git repo (if they have one) around the theme\n * dir. All state lives inside `.fluid-theme/`.\n *\n * The class only wraps the small handful of plumbing commands we\n * actually need: hash-object, cat-file, write-tree (via a temp index),\n * commit-tree, update-ref, and merge-file. Everything else stays out.\n */\nexport class ShadowRepo {\n // Cached \"does HEAD exist\" result. `pull`/`push` call `blobAtHead`\n // per file (potentially hundreds of times per invocation), and\n // spawning a `git rev-parse` subprocess each time is measurable\n // overhead. HEAD presence only flips one way per instance —\n // `commitState` sets it true after a successful `update-ref` — so\n // the cache stays correct without invalidation.\n private headExists: boolean | undefined = undefined;\n\n private constructor(\n private readonly themeRoot: string,\n private readonly gitDir: string,\n ) {}\n\n /**\n * Return a ShadowRepo bound to `themeId` for the given theme root.\n * The shadow is initialized on first use and re-initialized when\n * the caller passes a themeId different from the one previously\n * recorded — the merge base is only meaningful for the theme it\n * was captured against, so a cross-theme operation (pull A → push\n * B, or two pulls of different themes into one dir) starts from a\n * clean slate rather than pretending B's state matches A's HEAD.\n */\n static async open(themeRoot: string, themeId: number): Promise<ShadowRepo> {\n const shadowDir = join(themeRoot, \".fluid-theme\");\n const gitDir = join(shadowDir, \"repo\");\n const themeIdFile = join(shadowDir, \"theme-id\");\n\n // The stored themeId is written on every commit, so a shadow with\n // a HEAD but no theme-id file is one written by an older CLI —\n // treat it as \"unknown\" and wipe rather than trust it against\n // whatever theme the caller is now operating on.\n if (existsSync(gitDir)) {\n const stored = readStoredThemeId(themeIdFile);\n if (stored !== themeId) {\n rmSync(gitDir, { recursive: true, force: true });\n }\n }\n\n const repo = new ShadowRepo(themeRoot, gitDir);\n\n if (!existsSync(gitDir)) {\n mkdirSync(shadowDir, { recursive: true });\n await repo.git([\"init\", \"--bare\", \"-b\", \"main\", gitDir], {\n cwd: themeRoot,\n });\n }\n\n // Rewrite even when the dir already existed — a fresh init above\n // won't have this file yet, and a preserved dir may have missed a\n // previous write if the process crashed mid-way. Idempotent.\n writeFileSync(themeIdFile, `${themeId}\\n`, \"utf-8\");\n\n // Idempotent: keep `.fluid-theme/` invisible to the user's own git\n // repo. The nested `.gitignore` handles `git add .` inside the\n // shadow dir; the root-level entry handles `git status` on the\n // theme root, which otherwise still lists the untracked shadow\n // directory itself (only its *contents* are ignored by the nested\n // file).\n const shadowIgnore = join(shadowDir, \".gitignore\");\n if (!existsSync(shadowIgnore)) {\n writeFileSync(\n shadowIgnore,\n \"# Fluid CLI shadow repo — internal state, not for version control.\\n*\\n\",\n );\n }\n await ensureRootGitignoreHidesShadow(themeRoot);\n\n return repo;\n }\n\n /** True when the repo has at least one commit on `refs/heads/main`. */\n async hasHead(): Promise<boolean> {\n if (this.headExists !== undefined) return this.headExists;\n try {\n await this.git([\"rev-parse\", \"--verify\", \"HEAD\"]);\n this.headExists = true;\n } catch {\n this.headExists = false;\n }\n return this.headExists;\n }\n\n /**\n * Every path recorded under HEAD's tree, recursively. Callers use\n * this to detect local deletions (paths in HEAD, absent from the\n * working tree). Returns [] when HEAD has never been committed.\n */\n async headPaths(): Promise<string[]> {\n if (!(await this.hasHead())) return [];\n // -z is required, not cosmetic. Without it git honours core.quotePath\n // (default true) and renders any path containing a non-ASCII byte in\n // its C-quoted display form — `\"assets/caf\\303\\251.css\"`, quotes\n // included. That string matches nothing in the working tree, so\n // diffAgainstShadow reports the file as a local deletion and push\n // sends a DELETE for a key the server has never had. The failed\n // delete then blocks commitPushedState, so shadow HEAD never\n // advances and the phantom delete repeats on every later push.\n // -z also covers the rarer newline-in-path case; splitting on \"\\n\"\n // could not.\n const { stdout } = await this.git([\n \"ls-tree\",\n \"-r\",\n \"-z\",\n \"HEAD\",\n \"--name-only\",\n ]);\n return stdout\n .toString(\"utf8\")\n .split(\"\\0\")\n .filter((line) => line.length > 0);\n }\n\n /**\n * The content of `path` in HEAD's tree, or null when the path does\n * not exist there. Callers use this as the merge base for pull\n * conflict resolution.\n */\n async blobAtHead(path: string): Promise<Buffer | null> {\n if (!(await this.hasHead())) return null;\n try {\n const { stdout } = await this.git([\"cat-file\", \"-p\", `HEAD:${path}`]);\n return stdout;\n } catch {\n return null;\n }\n }\n\n /**\n * Write `content` as a blob in the shadow repo and return its sha.\n * Used by `commitState` to stage each file's content before the\n * `write-tree` call.\n */\n async writeBlob(content: string | Buffer): Promise<string> {\n const buf = typeof content === \"string\" ? Buffer.from(content) : content;\n const { stdout } = await this.git([\"hash-object\", \"-w\", \"--stdin\"], {\n input: buf,\n });\n return stdout.toString(\"utf8\").trim();\n }\n\n /**\n * Commit `files` as HEAD's new tree, threaded onto the current HEAD\n * as the parent. Uses a per-call temp index so a partial run can't\n * corrupt anything reachable from HEAD; the previous commit stays\n * intact until `update-ref` at the end.\n *\n * Returns the new commit sha.\n */\n async commitState(\n files: Array<{ path: string; sha: string }>,\n message: string,\n ): Promise<string> {\n const indexPath = mkdtempSync(join(tmpdir(), \"fluid-shadow-\")) + \"/index\";\n\n try {\n const indexArgs = [\"update-index\", \"--add\"];\n for (const { path, sha } of files) {\n indexArgs.push(\"--cacheinfo\", `100644,${sha},${path}`);\n }\n if (files.length > 0) {\n await this.git(indexArgs, { env: { GIT_INDEX_FILE: indexPath } });\n }\n\n const treeSha = (\n await this.git([\"write-tree\"], { env: { GIT_INDEX_FILE: indexPath } })\n ).stdout\n .toString(\"utf8\")\n .trim();\n\n const parent = (await this.hasHead())\n ? (await this.git([\"rev-parse\", \"HEAD\"])).stdout.toString(\"utf8\").trim()\n : null;\n\n const commitArgs = [\"commit-tree\", treeSha, \"-m\", message];\n if (parent) commitArgs.push(\"-p\", parent);\n\n const commitSha = (\n await this.git(commitArgs, {\n env: {\n GIT_AUTHOR_NAME: \"Fluid CLI\",\n GIT_AUTHOR_EMAIL: \"cli@fluid.app\",\n GIT_COMMITTER_NAME: \"Fluid CLI\",\n GIT_COMMITTER_EMAIL: \"cli@fluid.app\",\n },\n })\n ).stdout\n .toString(\"utf8\")\n .trim();\n\n await this.git([\"update-ref\", \"refs/heads/main\", commitSha]);\n this.headExists = true;\n return commitSha;\n } finally {\n // Best-effort — a stale index in tmp is harmless.\n try {\n rmSync(indexPath, { force: true });\n rmSync(indexPath.substring(0, indexPath.length - \"/index\".length), {\n recursive: true,\n force: true,\n });\n } catch {\n // ignore\n }\n }\n }\n\n /**\n * Three-way merge of `local` against `remote` with `base` as the\n * common ancestor. Returns the merged bytes and a flag when\n * `git merge-file` reported unresolved conflicts (i.e. the output\n * contains `<<<<<<<` markers for the reader to resolve).\n *\n * `base` is null when HEAD has never seen this path; we merge\n * against an empty base, which is what git itself does for a new\n * file added on both sides.\n *\n * `favor` maps to `git merge-file`'s `--ours` / `--theirs`: instead\n * of emitting `<<<<<<<` markers, conflicting hunks are resolved to\n * the local (`\"local\"` → `--ours`, local is file1) or remote\n * (`\"remote\"` → `--theirs`) side. The output then never contains\n * markers, so the result is reported conflict-free even when\n * merge-file's exit code still counts the auto-resolved hunks.\n */\n async merge3(\n base: Buffer | null,\n local: Buffer,\n remote: Buffer,\n favor?: \"local\" | \"remote\",\n ): Promise<{ merged: Buffer; hasConflicts: boolean }> {\n const dir = mkdtempSync(join(tmpdir(), \"fluid-merge-\"));\n const localPath = join(dir, \"local\");\n const basePath = join(dir, \"base\");\n const remotePath = join(dir, \"remote\");\n\n try {\n writeFileSync(localPath, local);\n writeFileSync(basePath, base ?? Buffer.alloc(0));\n writeFileSync(remotePath, remote);\n\n // `git merge-file -p` writes the merged result to stdout instead\n // of mutating `localPath` in place, and exits non-zero (= number\n // of unresolved conflicts) when it couldn't reconcile everything.\n // We treat any non-zero exit as \"conflicts present\" as long as\n // stdout is populated — the caller writes it to disk with markers.\n try {\n const { stdout } = await this.git([\n \"merge-file\",\n \"-p\",\n ...(favor === \"local\"\n ? [\"--ours\"]\n : favor === \"remote\"\n ? [\"--theirs\"]\n : []),\n \"-L\",\n \"local\",\n \"-L\",\n \"base\",\n \"-L\",\n \"remote\",\n localPath,\n basePath,\n remotePath,\n ]);\n return { merged: stdout, hasConflicts: false };\n } catch (err) {\n const e = err as {\n code?: number;\n stdout?: Buffer | string;\n stderr?: Buffer | string;\n };\n // `git merge-file` exit codes:\n // 0 clean merge (handled in the try branch above)\n // 1..127 count of unresolved conflict hunks; stdout is the\n // merged content with markers — treat as conflict.\n // >=128 git itself crashed (SIGSEGV, out-of-memory) —\n // stdout is empty, don't clobber the user's file.\n // negative usage error (bad args) — same, don't clobber.\n // Requiring both the conflict-range code AND a non-empty\n // stdout matches the comment above and stops a crash from\n // silently writing 0 bytes to disk.\n const merged =\n e.stdout instanceof Buffer\n ? e.stdout\n : e.stdout != null\n ? Buffer.from(e.stdout)\n : Buffer.alloc(0);\n const inConflictRange =\n typeof e.code === \"number\" && e.code >= 1 && e.code <= 127;\n // With a favor side, merge-file resolved every hunk itself —\n // the exit code still counts them, but the output carries no\n // markers, so it's a finished merge from the caller's view.\n // The stdout-length guard is dropped here: an auto-resolved\n // result can be legitimately empty (favoring a side that\n // deleted all content).\n if (inConflictRange && favor) {\n return { merged, hasConflicts: false };\n }\n if (inConflictRange && merged.length > 0) {\n return { merged, hasConflicts: true };\n }\n throw err;\n }\n } finally {\n try {\n rmSync(dir, { recursive: true, force: true });\n } catch {\n // ignore\n }\n }\n }\n\n /**\n * Snapshot the working-tree copy of `paths` into HEAD as a single\n * commit. Intended for the migration path: on the first pull with\n * the new CLI (no shadow repo yet, but a `.fluid-theme.json` with\n * checksums exists) we seed HEAD with whatever is on disk before\n * running the merge, so unmodified files fast-forward cleanly and\n * modified files show a diff.\n */\n async seedFromWorkingTree(\n files: Array<{ path: string; content: string | Buffer }>,\n message: string,\n ): Promise<void> {\n const entries: Array<{ path: string; sha: string }> = [];\n for (const { path, content } of files) {\n entries.push({ path, sha: await this.writeBlob(content) });\n }\n await this.commitState(entries, message);\n }\n\n private async git(\n args: string[],\n opts: {\n cwd?: string;\n input?: Buffer;\n env?: Record<string, string>;\n } = {},\n ): Promise<{ stdout: Buffer; stderr: Buffer }> {\n const fullArgs =\n args[0] === \"init\" ? args : [\"--git-dir\", this.gitDir, ...args];\n const child = spawn(\"git\", fullArgs, {\n cwd: opts.cwd ?? this.themeRoot,\n env: { ...process.env, ...opts.env },\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n if (opts.input) {\n child.stdin.write(opts.input);\n }\n child.stdin.end();\n\n return new Promise((resolve, reject) => {\n const stdout: Buffer[] = [];\n const stderr: Buffer[] = [];\n child.stdout.on(\"data\", (chunk: Buffer) => stdout.push(chunk));\n child.stderr.on(\"data\", (chunk: Buffer) => stderr.push(chunk));\n child.on(\"error\", reject);\n child.on(\"close\", (code) => {\n const out = Buffer.concat(stdout);\n const err = Buffer.concat(stderr);\n if (code === 0) {\n resolve({ stdout: out, stderr: err });\n } else {\n const e = new Error(\n `git ${args.join(\" \")} exited with ${code}: ${err.toString(\"utf8\")}`,\n ) as Error & { code: number; stdout: Buffer; stderr: Buffer };\n e.code = code ?? -1;\n e.stdout = out;\n e.stderr = err;\n reject(e);\n }\n });\n });\n }\n}\n\n/**\n * Content-type check the pull command uses to decide whether a file\n * is safe to run through `merge3` (line-based) or must fall back to\n * whole-file \"either/or\" resolution (binary).\n */\nexport function looksBinary(content: Buffer): boolean {\n // Same heuristic git uses for `core.autocrlf` detection: any NUL in\n // the first 8000 bytes counts as binary.\n const scan = content.subarray(0, Math.min(content.length, 8000));\n return scan.includes(0);\n}\n\n/** Best-effort readFile that returns null when the file does not exist. */\nexport function readIfExists(path: string): Buffer | null {\n try {\n return readFileSync(path);\n } catch {\n return null;\n }\n}\n\n/**\n * Parse the numeric theme id from `.fluid-theme/theme-id`. Returns\n * null when the file is missing or the contents don't parse cleanly;\n * `open` treats that as \"unknown theme\" and rebuilds the shadow.\n */\nfunction readStoredThemeId(themeIdFile: string): number | null {\n try {\n const stored = parseInt(readFileSync(themeIdFile, \"utf-8\").trim(), 10);\n return Number.isFinite(stored) ? stored : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Append `.fluid-theme/` to the theme root's `.gitignore` when the\n * theme dir sits inside a git working tree and the entry isn't\n * already there. Skipped when the user isn't in a git repo — no\n * point manufacturing a `.gitignore` for someone who doesn't use\n * git. Idempotent — a second call is a no-op.\n */\nasync function ensureRootGitignoreHidesShadow(\n themeRoot: string,\n): Promise<void> {\n const insideGit = await new Promise<boolean>((resolve) => {\n const child = spawn(\"git\", [\"rev-parse\", \"--is-inside-work-tree\"], {\n cwd: themeRoot,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n child.on(\"close\", (code) => resolve(code === 0));\n child.on(\"error\", () => resolve(false));\n });\n if (!insideGit) return;\n\n const gitignorePath = join(themeRoot, \".gitignore\");\n let existing = \"\";\n try {\n existing = readFileSync(gitignorePath, \"utf-8\");\n } catch {\n existing = \"\";\n }\n\n const lines = existing.split(\"\\n\").map((line) => line.trim());\n // Match both `.fluid-theme/` and `.fluid-theme` — either shape hides\n // the dir from `git status`.\n if (lines.includes(\".fluid-theme/\") || lines.includes(\".fluid-theme\")) {\n return;\n }\n\n const separator =\n existing.length === 0 || existing.endsWith(\"\\n\") ? \"\" : \"\\n\";\n writeFileSync(\n gitignorePath,\n `${existing}${separator}.fluid-theme/\\n`,\n \"utf-8\",\n );\n}\n","import chalk from \"chalk\";\nimport prompts from \"prompts\";\nimport type { createApiClient } from \"./api.js\";\nimport { themes, type components } from \"@fluid-app/themes-api-client\";\n\nexport type ApplicationTheme = components[\"schemas\"][\"ApplicationTheme\"];\n\nconst PAGE_SIZE = 50;\nconst LOAD_MORE_VALUE = -1;\n\nfunction themeLabel(t: ApplicationTheme): string {\n const active = t.status === \"active\" ? ` ${chalk.green(\"[active]\")}` : \"\";\n return `${t.name} (#${t.id})${active}`;\n}\n\nfunction themeChoices(\n themeList: ApplicationTheme[],\n hasMore: boolean,\n): prompts.Choice[] {\n const choices: prompts.Choice[] = themeList.map((t) => ({\n title: themeLabel(t),\n value: t.id,\n }));\n if (hasMore) {\n choices.push({\n title: chalk.dim(`── Load more themes ──`),\n value: LOAD_MORE_VALUE,\n });\n }\n return choices;\n}\n\nasync function fetchThemesPage(\n api: ReturnType<typeof createApiClient>,\n page: number,\n searchQuery?: string,\n): Promise<{\n themes: ApplicationTheme[];\n hasMore: boolean;\n}> {\n const body = await themes.listApplicationThemes(api, {\n per_page: PAGE_SIZE,\n page,\n ...(searchQuery ? { search_query: searchQuery } : {}),\n });\n const list = body.application_themes ?? [];\n const totalPages = body.meta?.total_pages ?? 1;\n return { themes: list, hasMore: page < totalPages };\n}\n\nexport async function selectTheme(\n api: ReturnType<typeof createApiClient>,\n message: string,\n): Promise<ApplicationTheme> {\n const allThemes: ApplicationTheme[] = [];\n let page = 1;\n let hasMore = true;\n let initialIndex = 0;\n\n // Search cache — persists across suggest calls\n let searchQuery = \"\";\n let searchResults: ApplicationTheme[] = [];\n\n while (true) {\n if (hasMore && allThemes.length < page * PAGE_SIZE) {\n const result = await fetchThemesPage(api, page);\n allThemes.push(...result.themes);\n hasMore = result.hasMore;\n }\n\n if (!allThemes.length) {\n console.error(\"No themes found.\");\n process.exit(1);\n }\n\n const choices = themeChoices(allThemes, hasMore);\n\n const { id } = await prompts(\n {\n type: \"autocomplete\",\n name: \"id\",\n message,\n initial: initialIndex,\n choices,\n suggest: async (input: string, choices: prompts.Choice[]) => {\n if (!input) {\n searchQuery = \"\";\n searchResults = [];\n return choices;\n }\n\n if (input !== searchQuery) {\n searchQuery = input;\n try {\n const result = await fetchThemesPage(api, 1, input);\n searchResults = result.themes;\n } catch {\n searchResults = [];\n }\n }\n\n return searchResults.map((t) => ({\n title: themeLabel(t),\n value: t.id,\n }));\n },\n },\n { onCancel: () => process.exit(130) },\n );\n\n if (id === LOAD_MORE_VALUE) {\n initialIndex = allThemes.length;\n page++;\n continue;\n }\n\n if (!id) {\n console.error(\"No theme selected.\");\n process.exit(1);\n }\n\n // Check loaded themes first, then search results\n const found =\n allThemes.find((t) => t.id === id) ??\n searchResults.find((t) => t.id === id);\n if (found) return found;\n\n // Fetch directly by ID as fallback\n const body = await themes.getApplicationTheme(api, id);\n return body.application_theme;\n }\n}\n\nexport async function findTheme(\n api: ReturnType<typeof createApiClient>,\n identifier: string,\n): Promise<ApplicationTheme> {\n // Try ID lookup first\n const idNum = Number(identifier);\n if (Number.isInteger(idNum) && idNum > 0) {\n try {\n const body = await themes.getApplicationTheme(api, idNum);\n if (body.application_theme) return body.application_theme;\n } catch {\n // Not found by ID, fall through to search\n }\n }\n\n // Search by name via API with pagination\n let page = 1;\n let hasMore = true;\n while (hasMore) {\n const result = await fetchThemesPage(api, page, identifier);\n const found = result.themes.find(\n (t) => t.name.toLowerCase() === identifier.toLowerCase(),\n );\n if (found) return found;\n hasMore = result.hasMore;\n page++;\n }\n\n console.error(`No theme found with identifier: ${identifier}`);\n process.exit(1);\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join, relative, resolve, sep } from \"node:path\";\n\nexport interface FluidWorkspace {\n /** Absolute path to the workspace root (where .fluid-workspace.json lives) */\n root: string;\n /** Parsed workspace config */\n config: WorkspaceConfig;\n}\n\ninterface WorkspaceConfig {\n type: string;\n version: number;\n}\n\nconst WORKSPACE_FILE = \".fluid-workspace.json\";\n\n/**\n * Walk up from `startDir` looking for `.fluid-workspace.json`.\n * Returns the workspace info if found, or `null` if not in a workspace.\n */\nexport function findWorkspace(startDir?: string): FluidWorkspace | null {\n let dir = resolve(startDir ?? process.cwd());\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const candidate = join(dir, WORKSPACE_FILE);\n if (existsSync(candidate)) {\n try {\n const raw = readFileSync(candidate, \"utf-8\");\n const config = JSON.parse(raw) as WorkspaceConfig;\n return { root: dir, config };\n } catch {\n return null;\n }\n }\n const parent = dirname(dir);\n if (parent === dir) break; // reached filesystem root\n dir = parent;\n }\n\n return null;\n}\n\n/**\n * If cwd is already inside `{workspace}/local/{company}/...`, return that\n * theme root directory. Otherwise return null.\n *\n * Examples (workspace root = /code/fluid-theme-dev):\n * cwd = /code/fluid-theme-dev/local/acme-co → /code/fluid-theme-dev/local/acme-co\n * cwd = /code/fluid-theme-dev/local/acme-co/templates → /code/fluid-theme-dev/local/acme-co\n * cwd = /code/fluid-theme-dev → null\n * cwd = /code/fluid-theme-dev/local → null\n */\nexport function resolveThemeRootFromCwd(\n workspace: FluidWorkspace,\n): string | null {\n const cwd = resolve(process.cwd());\n const localDir = join(workspace.root, \"local\");\n const rel = relative(localDir, cwd);\n\n // Not under local/ at all, or exactly at local/\n if (rel.startsWith(\"..\") || rel === \".\") return null;\n\n // rel is like \"acme-co\" or \"acme-co/templates/subfolder\"\n // The theme root is the first segment: local/{company}\n const firstSegment = rel.split(sep)[0];\n if (!firstSegment) return null;\n\n return join(localDir, firstSegment);\n}\n","import { Command } from \"commander\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { readThemeConfig } from \"../theme-config.js\";\nimport {\n devThemeKey,\n getDevTheme,\n setDevTheme,\n setLastDevThemeId,\n clearDevTheme,\n type DevThemeRef,\n} from \"../plugin-state.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { startDevServer } from \"../theme/dev-server/index.js\";\nimport { ThemeAssetManifest } from \"../theme/asset-manifest.js\";\nimport {\n devRemoteStateFromSourceShadow,\n readDevRemoteBaseline,\n removeDevRemoteBaseline,\n writeDevRemoteBaseline,\n} from \"../theme/dev-remote-baseline.js\";\nimport { ShadowRepo } from \"../theme/shadow-repo.js\";\nimport {\n checkPortAvailable,\n PortInUseError,\n} from \"../theme/dev-server/port-preflight.js\";\nimport { isApiError, themes } from \"@fluid-app/themes-api-client\";\nimport { findTheme, type ApplicationTheme } from \"../theme-picker.js\";\nimport { findWorkspace, resolveThemeRootFromCwd } from \"../workspace.js\";\n\ninterface CompanyMe {\n data: { company: { subdomain?: string; name?: string } };\n}\n\n/** Whether this invocation may read and persist the dev remote baseline. */\nexport function devRemoteBaselineEnabled(explicitTheme: boolean): boolean {\n return (\n !explicitTheme && process.env[\"FLUID_THEME_DEV_DISABLE_SHADOW_SYNC\"] !== \"1\"\n );\n}\n\n/**\n * Create the isolated theme used by `theme dev`.\n *\n * A checkout from `theme pull` has a source theme id. New servers clone that\n * source by reference, preserving its DAM/ImageKit assets without moving\n * bytes. A 404/405 keeps older deployments compatible with the established\n * empty-theme flow; other failures must remain visible to the developer.\n */\nexport async function createDevelopmentTheme(\n api: ReturnType<typeof createApiClient>,\n sourceThemeId: number | undefined,\n name: string,\n): Promise<{ theme: ApplicationTheme; referenceCloned: boolean }> {\n if (sourceThemeId !== undefined) {\n try {\n const body = await themes.cloneApplicationThemeForDevelopment(\n api,\n sourceThemeId,\n { application_theme: { name } },\n );\n return { theme: body.application_theme, referenceCloned: true };\n } catch (error) {\n if (\n !isApiError(error) ||\n (error.status !== 404 && error.status !== 405)\n ) {\n throw error;\n }\n\n console.warn(\n \"Server-side theme cloning is unavailable; falling back to an empty development theme. The first sync may take longer.\",\n );\n }\n }\n\n const body = await themes.createApplicationTheme(api, {\n application_theme: { name, status: \"development\" },\n });\n return { theme: body.application_theme, referenceCloned: false };\n}\n\nasync function ensureDevTheme(\n api: ReturnType<typeof createApiClient>,\n projectKey: string,\n identifier?: string,\n sourceThemeId?: number,\n): Promise<{ theme: ApplicationTheme; referenceCloned: boolean }> {\n if (identifier) {\n const theme = await findTheme(api, identifier);\n // Keep `navigate` pointed at whatever the dev server is actually serving.\n setLastDevThemeId(theme.id);\n return { theme, referenceCloned: false };\n }\n\n // Reuse this project's stored dev theme if it still exists and is still a\n // development theme (a published/promoted theme must not be edited in place).\n const stored = getDevTheme(projectKey);\n // A checkout pulled from a different source needs an isolated sandbox of\n // its own. Older stored entries have no source id, so they are retained only\n // for worktrees that were not pulled from a remote theme.\n if (stored && stored.sourceThemeId === sourceThemeId) {\n try {\n const body = await themes.getApplicationTheme(api, stored.id);\n const existing = body.application_theme;\n if (existing && existing.status === \"development\") {\n console.log(`Using existing dev theme #${existing.id}`);\n // Refresh the stored name and mark it most-recent for `navigate`.\n setDevTheme(projectKey, {\n ...stored,\n id: existing.id,\n name: existing.name,\n ...(sourceThemeId === undefined ? {} : { sourceThemeId }),\n });\n return { theme: existing, referenceCloned: false };\n }\n } catch {\n // Theme no longer exists — fall through to create a new one.\n }\n // Stored theme is gone or no longer a dev theme; forget it.\n clearDevTheme(projectKey);\n }\n\n // Create a new development theme\n const { hostname } = await import(\"node:os\");\n const host = hostname().split(\".\")[0] ?? \"dev\";\n const name =\n `Development (${host}-${Math.random().toString(36).slice(2, 8)})`.slice(\n 0,\n 50,\n );\n\n const creation = await createDevelopmentTheme(api, sourceThemeId, name);\n const { theme } = creation;\n const devTheme: DevThemeRef = {\n id: theme.id,\n name: theme.name,\n ...(sourceThemeId === undefined ? {} : { sourceThemeId }),\n };\n setDevTheme(projectKey, devTheme);\n console.log(`Created dev theme: ${theme.name} (#${theme.id})`);\n return creation;\n}\n\nexport function createDevCommand(): Command {\n return new Command(\"dev\")\n .description(\"Start the theme dev server with hot reload\")\n .option(\"--host <host>\", \"Local server host\", \"127.0.0.1\")\n .option(\"--port <port>\", \"Local server port\", \"9292\")\n .option(\n \"-t, --theme <name-or-id>\",\n \"Use an existing theme instead of dev theme\",\n )\n .option(\"-f, --force\", \"Skip schema validation on upload\")\n .option(\"--live-reload <mode>\", \"Reload mode: full-page | off\", \"full-page\")\n .option(\"--navigate\", \"Open browser navigator after server starts\")\n .option(\"--root <path>\", \"Theme root directory\", \".\")\n .action(\n async (opts: {\n host: string;\n port: string;\n theme?: string;\n force?: boolean;\n liveReload: string;\n navigate?: boolean;\n root: string;\n }) => {\n requireToken();\n\n // If no explicit --root and we're inside a workspace, resolve to the theme root\n let rootPath = opts.root;\n if (rootPath === \".\") {\n const workspace = findWorkspace();\n if (workspace) {\n rootPath = resolveThemeRootFromCwd(workspace) ?? rootPath;\n }\n }\n\n const themeRoot = new ThemeRoot(rootPath);\n if (!themeRoot.isValid()) {\n console.error(`'${rootPath}' does not look like a theme directory.`);\n process.exit(1);\n }\n\n const port = Number(opts.port);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n console.error(\n `Invalid port: '${opts.port}'. Must be an integer between 1 and 65535.`,\n );\n process.exit(1);\n }\n\n // Fail fast if the port is already taken — everything below this\n // point (resolving/creating the dev theme, the initial sync) is\n // expensive and would otherwise run to completion only to crash\n // with a raw EADDRINUSE stack trace when the server finally binds.\n try {\n await checkPortAvailable(opts.host, port);\n } catch (e) {\n if (e instanceof PortInUseError) {\n console.error(e.message);\n } else {\n console.error(`Failed to check port availability: ${e}`);\n }\n process.exit(1);\n }\n\n const reloadMode = opts.liveReload === \"off\" ? \"off\" : \"full-page\";\n const api = createApiClient();\n const config = readThemeConfig(themeRoot.root);\n\n // Use company from .fluid-theme.json if available, otherwise fetch\n let company: string;\n if (config?.company) {\n company = config.company;\n } else {\n const companyRes = await api.get<CompanyMe>(\n \"/api/company/v1/companies/me\",\n );\n company = companyRes.data?.company?.subdomain ?? \"\";\n if (!company) {\n console.error(\n \"Could not determine company subdomain. Make sure your token is valid.\",\n );\n process.exit(1);\n }\n }\n\n // Always iterate on an isolated dev theme: reuse the stored one or\n // create a fresh `development` theme. `--theme` is the explicit\n // escape hatch for targeting an existing theme. A pulled theme id is\n // never a sync target: it only seeds a new isolated reference clone.\n const projectKey = devThemeKey(company, themeRoot.root);\n const devTarget = opts.theme\n ? await ensureDevTheme(api, projectKey, opts.theme)\n : await ensureDevTheme(api, projectKey, undefined, config?.themeId);\n const { theme } = devTarget;\n const assetManifest = new ThemeAssetManifest(themeRoot.root);\n const baselineEnabled = devRemoteBaselineEnabled(Boolean(opts.theme));\n let initialRemoteState = baselineEnabled\n ? readDevRemoteBaseline(\n themeRoot.root,\n theme.id,\n assetManifest.fingerprint(),\n )\n : null;\n if (\n !initialRemoteState &&\n baselineEnabled &&\n devTarget.referenceCloned &&\n config?.themeId &&\n config.baseSha\n ) {\n try {\n const sourceShadow = await ShadowRepo.open(\n themeRoot.root,\n config.themeId,\n );\n initialRemoteState = await devRemoteStateFromSourceShadow(\n themeRoot,\n sourceShadow,\n theme.id,\n config.baseSha,\n );\n if (initialRemoteState) {\n writeDevRemoteBaseline(themeRoot.root, initialRemoteState);\n }\n } catch {\n initialRemoteState = null;\n }\n }\n const editorUrl = `https://admin.fluid.app/themes/${theme.id}/editor`;\n\n let stop: (() => void) | undefined;\n\n const cleanup = () => {\n stop?.();\n process.exit(0);\n };\n process.on(\"SIGINT\", cleanup);\n process.on(\"SIGTERM\", cleanup);\n\n stop = await startDevServer(\n api,\n {\n id: theme.id,\n name: theme.name,\n company,\n editorUrl,\n },\n themeRoot,\n {\n host: opts.host,\n port,\n reloadMode,\n validate: !opts.force,\n ...(initialRemoteState ? { initialSync: initialRemoteState } : {}),\n ...(baselineEnabled\n ? {\n onRemoteState: (state) =>\n writeDevRemoteBaseline(themeRoot.root, state),\n onRemoteStateInvalidated: () =>\n removeDevRemoteBaseline(themeRoot.root),\n }\n : {}),\n },\n (address) => {\n console.log(`\\n Dev server: ${address}`);\n console.log(` Web editor: ${editorUrl}`);\n console.log(\"\\n Watching for file changes…\\n\");\n\n if (opts.navigate) {\n import(\"open\").then((m) => m.default(`${address}/home`));\n }\n },\n );\n\n // Keep process alive\n await new Promise(() => {});\n },\n );\n}\n","import type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport {\n MANAGED_ASSET_SHADOW_SENTINEL,\n ThemeAssetManifest,\n} from \"./asset-manifest.js\";\nimport { ShadowRepo, readIfExists } from \"./shadow-repo.js\";\nimport { isStylesheetKey } from \"./stylesheet-keys.js\";\nimport { assertNoCaseCollisions } from \"./case-collisions.js\";\n\nexport interface DiffSet {\n /** Files whose local content differs from shadow HEAD — the ones to send. */\n changed: ThemeFile[];\n /** Paths present in shadow HEAD but no longer on disk — deletions to send. */\n deleted: string[];\n}\n\n/**\n * Compute what changed locally since the last time the shadow repo\n * committed a state. Replaces the sha256 `checksums` map: shadow HEAD\n * is the source of truth for \"what the CLI last saw the server have\".\n *\n * Files whose local bytes are byte-identical to their HEAD blob are\n * skipped; anything else — new, modified, or a locally-deleted path\n * that HEAD still has — is included.\n */\nexport async function diffAgainstShadow(\n themeRoot: ThemeRoot,\n shadow: ShadowRepo,\n): Promise<DiffSet> {\n const changed: ThemeFile[] = [];\n const deleted: string[] = [];\n\n const localFiles = themeRoot.files();\n const localByKey = new Map<string, ThemeFile>();\n const assetManifest = new ThemeAssetManifest(themeRoot.root);\n let headPaths: string[];\n try {\n headPaths = await shadow.headPaths();\n } catch (error) {\n throw new Error(\"Could not read the local theme shadow\", { cause: error });\n }\n\n assertNoCaseCollisions([\n ...localFiles.map((file) => file.relativePath),\n ...headPaths,\n ]);\n const headPathSet = new Set(headPaths);\n\n for (const file of localFiles) {\n if (!file.exists) continue;\n localByKey.set(file.relativePath, file);\n\n const headBlob = await shadow.blobAtHead(file.relativePath);\n if (!headBlob && headPathSet.has(file.relativePath)) {\n throw new Error(\n `Could not read the local theme shadow: ${file.relativePath}`,\n );\n }\n const localBuf = file.isText ? Buffer.from(file.read()) : file.readBinary();\n if (headBlob && headBlob.equals(localBuf)) continue;\n\n changed.push(file);\n }\n\n for (const key of headPaths) {\n if (localByKey.has(key)) continue;\n if (themeRoot.ignore.ignore(key)) continue;\n // A manifest-backed asset is deliberately absent from disk. Its URL is\n // still authoritative and must never be emitted as a remote deletion.\n if (assetManifest.has(key)) continue;\n if (isStylesheetKey(key)) continue; // hidden from the API surface\n deleted.push(key);\n }\n\n return { changed, deleted };\n}\n\n/**\n * Refuse a push when any working file still contains a conflict marker\n * from a previous pull. Mirrors git's \"you have unresolved conflicts;\n * fix them and re-run\" behavior — the whole point of writing markers\n * on pull was to hand resolution to the user, so we can't send them\n * upstream.\n */\nexport function findUnresolvedConflicts(files: ThemeFile[]): string[] {\n const flagged: string[] = [];\n for (const file of files) {\n if (!file.isText) continue;\n const buf = readIfExists(file.absolutePath);\n if (!buf) continue;\n if (containsConflictMarker(buf)) flagged.push(file.relativePath);\n }\n return flagged;\n}\n\n/**\n * Stable, machine-readable one-liner for non-interactive callers\n * (Mist Desktop's publish flow parses push output). Uploading marker-\n * bearing files to a live theme is never acceptable, so `--auto-\n * baseline` pushes still refuse — but they emit this line so the\n * desktop can surface WHICH files block the publish instead of a\n * dead-end wall of prose. Format:\n *\n * FLUID_THEME_PUSH_BLOCKED code=conflict_markers files=a.liquid,b.json\n */\nexport function conflictMarkerBlockLine(files: string[]): string {\n return `FLUID_THEME_PUSH_BLOCKED code=conflict_markers files=${files.join(\",\")}`;\n}\n\nconst CONFLICT_START = Buffer.from(\"<<<<<<<\");\nconst CONFLICT_MID = Buffer.from(\"=======\");\nconst CONFLICT_END = Buffer.from(\">>>>>>>\");\n\n/**\n * A file counts as unresolved when it contains all three marker\n * shapes: `<<<<<<<`, `=======`, and `>>>>>>>`. Requiring all three\n * avoids false positives — a line of equals signs alone (e.g. inside\n * an ASCII table in a template comment) doesn't trip the guard.\n */\nfunction containsConflictMarker(buf: Buffer): boolean {\n return (\n buf.includes(CONFLICT_START) &&\n buf.includes(CONFLICT_MID) &&\n buf.includes(CONFLICT_END)\n );\n}\n\n/**\n * Commit the current working-tree state to shadow HEAD after a\n * successful push. Ensures the next pull's merge base is the state\n * we know the server just accepted.\n */\nexport async function commitPushedState(\n themeRoot: ThemeRoot,\n shadow: ShadowRepo,\n message: string,\n): Promise<void> {\n const entries: Array<{ path: string; sha: string }> = [];\n const assetManifest = new ThemeAssetManifest(themeRoot.root);\n const localKeys = new Set<string>();\n for (const file of themeRoot.files()) {\n if (!file.exists) continue;\n localKeys.add(file.relativePath);\n // A manifest entry normally has no file at all. If bytes are present (for\n // example after a metadata fallback), they remain authoritative until a\n // successful upload can externalize them again.\n const buf = file.isText ? Buffer.from(file.read()) : file.readBinary();\n entries.push({ path: file.relativePath, sha: await shadow.writeBlob(buf) });\n }\n let managedAssetSentinelSha: string | undefined;\n for (const key of assetManifest.keys()) {\n if (localKeys.has(key)) continue;\n managedAssetSentinelSha ??= await shadow.writeBlob(\n MANAGED_ASSET_SHADOW_SENTINEL,\n );\n entries.push({\n path: key,\n sha: managedAssetSentinelSha,\n });\n }\n // Commit an empty tree too when HEAD exists: a successful final deletion\n // must clear its old path/sentinel so later pushes do not repeat it.\n if (entries.length > 0 || (await shadow.hasHead())) {\n await shadow.commitState(entries, message);\n }\n}\n\n/**\n * A target can already have every URL-backed FileResource (for example from a\n * reference clone), while this checkout's manifest still names its old source\n * theme. Adopt the target and clear any legacy binary from shadow even though\n * no remote write was necessary.\n */\nexport async function finalizeManifestOnlyPush(\n syncer: { repointManagedAssetsToCurrentTheme(): void },\n themeRoot: ThemeRoot,\n shadow: ShadowRepo,\n): Promise<void> {\n syncer.repointManagedAssetsToCurrentTheme();\n await commitPushedState(\n themeRoot,\n shadow,\n `push @ ${new Date().toISOString()}`,\n );\n}\n","import { sep } from \"node:path\";\nimport type { components } from \"@fluid-app/themes-api-client\";\nimport type { ShadowRepo } from \"./shadow-repo.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport type { FetchBinary } from \"./merge-pull.js\";\n\ntype RemoteResource = components[\"schemas\"][\"ApplicationThemeResource\"];\n\nexport interface SeedBaselineResult {\n /** False when a baseline already existed (HEAD present) — e.g. the\n * legacy checksum-era migration already seeded it. The migration\n * always wins over server seeding: a migrated HEAD carries the\n * local \"unmodified since last sync\" blobs, which is a better\n * merge base than the server's current state. */\n seeded: boolean;\n /** Paths recorded into the baseline commit. */\n recorded: string[];\n /** Server paths with no local counterpart, deliberately left OUT of\n * the baseline (see invariant below). */\n serverOnly: string[];\n /** Non-fatal per-file problems (e.g. a binary download failed). */\n errors: string[];\n}\n\n/**\n * `fluid theme push --auto-baseline`: when a theme directory has no\n * shadow baseline (scaffold that never pulled, or a dir last synced\n * by the checksum-era CLI whose migration had nothing to seed),\n * record the server's CURRENT state as the baseline commit so the\n * normal push diff (local vs baseline) can run.\n *\n * Invariants:\n *\n * 1. **The working tree is never touched.** Baseline recording writes\n * blobs into the bare shadow repo only; not a single byte on disk\n * changes. Local files identical to the server simply won't diff;\n * files that differ (or exist only locally) will push.\n *\n * 2. **Server-only files are never deleted.** Paths that exist on the\n * server but not locally are deliberately excluded from the\n * baseline commit. `diffAgainstShadow` reports deletions as \"in\n * HEAD but not on disk\", so putting server-only paths into HEAD\n * would mark them for remote deletion — on THIS push (or worse, a\n * later one) — for files the user never had. Excluding them makes\n * the diff structurally unable to delete them; the next `pull`\n * materializes them locally and records them for real.\n *\n * Bandwidth note: a server file whose sha256 checksum matches the\n * local file's is recorded from the LOCAL bytes (identical by\n * definition), so binary assets that are already in sync are never\n * downloaded just to seed the baseline.\n *\n * No-op (`seeded: false`) when HEAD already exists.\n */\nexport async function seedBaselineFromServer(input: {\n shadow: ShadowRepo;\n themeRoot: ThemeRoot;\n remote: RemoteResource[];\n fetchBinary: FetchBinary;\n message: string;\n}): Promise<SeedBaselineResult> {\n const { shadow, themeRoot, remote, fetchBinary, message } = input;\n const result: SeedBaselineResult = {\n seeded: false,\n recorded: [],\n serverOnly: [],\n errors: [],\n };\n\n if (await shadow.hasHead()) return result;\n\n const entries: Array<{ path: string; sha: string }> = [];\n\n for (const resource of remote) {\n const file = themeRoot.file(resource.key);\n\n // Same traversal guard as mergePull — never trust remote keys.\n if (!file.absolutePath.startsWith(themeRoot.root + sep)) {\n result.errors.push(`Baseline ${resource.key}: path traversal detected`);\n continue;\n }\n\n if (!file.exists) {\n result.serverOnly.push(resource.key);\n continue;\n }\n\n let content: Buffer | null;\n if (resource.checksum && file.checksum() === resource.checksum) {\n // Identical to the server — local bytes ARE the server bytes.\n content = file.isText ? Buffer.from(file.read()) : file.readBinary();\n } else {\n try {\n content = await materializeRemote(resource, fetchBinary);\n } catch (e) {\n result.errors.push(\n `Baseline ${resource.key}: ${e instanceof Error ? e.message : String(e)}`,\n );\n continue;\n }\n }\n if (content == null) {\n // Nothing to record (empty resource) — the local file will be\n // treated as new and pushed, which is the honest outcome.\n continue;\n }\n\n entries.push({ path: resource.key, sha: await shadow.writeBlob(content) });\n result.recorded.push(resource.key);\n }\n\n if (entries.length > 0) {\n await shadow.commitState(entries, message);\n result.seeded = true;\n }\n\n return result;\n}\n\nasync function materializeRemote(\n resource: RemoteResource,\n fetchBinary: FetchBinary,\n): Promise<Buffer | null> {\n if (resource.resource_type === \"FileResource\" && resource.url) {\n return fetchBinary(resource.url);\n }\n if (resource.content == null) return null;\n const text =\n typeof resource.content === \"string\"\n ? resource.content\n : JSON.stringify(resource.content);\n return Buffer.from(text);\n}\n","import { readLegacyThemeConfig } from \"../theme-config.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport type { ShadowRepo } from \"./shadow-repo.js\";\n\n/**\n * On first pull after upgrading from a checksum-era CLI, the shadow\n * repo starts with no HEAD. `mergePull` would then run every diverged\n * file through a null-base merge — even files the user never touched\n * locally — producing spurious `<<<<<<<` markers for every file the\n * server updated since the last pull.\n *\n * Recover a real merge base by trusting the legacy sha256 checksums:\n * any local file whose content still matches its stored checksum is\n * \"unmodified since last pull\" and can be committed as HEAD. Files\n * whose local sha256 diverges from the stored checksum stay\n * unseeded — we don't have their pre-modification content, so a\n * null-base merge (marker-first UX) is the honest fallback for them.\n *\n * Idempotent: no-op when HEAD already exists, when there is no\n * legacy config, when the config is for a different theme, or when\n * the checksums map is empty. Runs before `mergePull` so its base\n * lookups see the seeded tree.\n */\nexport async function migrateLegacyChecksumsIntoShadow(input: {\n shadow: ShadowRepo;\n themeRoot: ThemeRoot;\n absoluteRoot: string;\n themeId: number;\n}): Promise<void> {\n const { shadow, themeRoot, absoluteRoot, themeId } = input;\n\n if (await shadow.hasHead()) return;\n\n const legacy = readLegacyThemeConfig(absoluteRoot);\n if (!legacy) return;\n if (legacy.themeId !== themeId) return;\n if (!legacy.checksums || Object.keys(legacy.checksums).length === 0) return;\n\n const seed: Array<{ path: string; content: Buffer }> = [];\n for (const file of themeRoot.files()) {\n if (!file.exists) continue;\n const stored = legacy.checksums[file.relativePath];\n if (!stored) continue;\n if (file.checksum() !== stored) continue;\n\n const content = file.isText ? Buffer.from(file.read()) : file.readBinary();\n seed.push({ path: file.relativePath, content });\n }\n\n if (seed.length === 0) return;\n\n await shadow.seedFromWorkingTree(\n seed,\n `migrate from checksum-era CLI @ ${new Date().toISOString()}`,\n );\n}\n","import type { ApiClient } from \"../api.js\";\nimport type { ChangeEntry, GitSyncActor } from \"@fluid-app/fluid-cli\";\nimport {\n gitSyncActorFromMe,\n gitSyncCommitSubject,\n resolveGitSyncActor,\n summarizeChanges,\n} from \"@fluid-app/fluid-cli\";\n\n/**\n * User-stamped shadow-repo commit subjects, in the canonical GitSync\n * format shared with the mist / portal / widget CLIs and the Ruby\n * adapters (see `@fluid-app/fluid-cli`'s git-sync/commit-subject.ts):\n *\n * Mike Tingey: Push theme Aurora · 2026-07-22 05:10:33 (0182d21e-…)\n *\n * When the actor is unknown (offline, /api/me down, token weirdness) the\n * commit is attributed to `Fluid` in the same shape, so consumers need\n * one grammar rather than two:\n *\n * Fluid: Push theme Aurora · 2026-07-22 05:10:33\n *\n * Shadow repos written by older CLIs still hold the pre-format machine\n * wording (`push @ <iso>`). We no longer emit it, but Mist Desktop's\n * History popover still parses it so those repos keep rendering.\n */\n\nexport type SyncVerb =\n | \"Push\"\n | \"Pull\"\n | \"Baseline from server\"\n | \"Snapshot before pull\";\n\n/** Human action per verb, used as the canonical subject's action. */\nconst BASE_MESSAGE: Record<SyncVerb, string> = {\n Push: \"Push from Fluid CLI\",\n Pull: \"Pull from Fluid CLI\",\n \"Baseline from server\": \"Baseline from server\",\n \"Snapshot before pull\": \"Snapshot before pull\",\n};\n\n/** Re-exported under the local name the theme CLI has always used. */\nexport type SyncActor = GitSyncActor;\n\n/**\n * Build a shadow-repo commit subject.\n *\n * `changes` (when supplied) replaces the per-verb constant with a\n * description of what the sync actually carried, so the shadow log reads\n * `Update 3 files in templates` rather than `Push from Fluid CLI` on\n * every entry. Falls back to the constant for an empty change set — a\n * baseline or a no-op sync still deserves a sensible subject.\n */\nexport function syncCommitSubject(\n verb: SyncVerb,\n actor: SyncActor | null,\n when: Date = new Date(),\n changes: readonly ChangeEntry[] = [],\n): string {\n const action = summarizeChanges(changes) ?? BASE_MESSAGE[verb];\n return gitSyncCommitSubject(action, actor, when);\n}\n\n/** Resolve a `/api/me` body to a commit actor. Exported for tests.\n * Delegates to the shared resolver so the name fallback chain\n * (full_name → first+last → email → `user-<id>`) can't drift between\n * CLIs. Null when the body carries no usable identity — the caller then\n * writes a `Fluid:` system commit. */\nexport function actorFromMe(\n raw: Parameters<typeof gitSyncActorFromMe>[0],\n): SyncActor | null {\n return gitSyncActorFromMe(raw);\n}\n\n/**\n * Best-effort fetch of the signed-in user for commit stamping. Races\n * `/api/me` against a short timeout and NEVER rejects — a sync must\n * not block or fail because the identity lookup did. Callers kick\n * this off early and await it only at commit time.\n */\nexport async function fetchSyncActor(\n api: ApiClient,\n timeoutMs = 5_000,\n): Promise<SyncActor | null> {\n // Served from the profile cache after the first sync; /api/me only\n // fires on a miss.\n return resolveGitSyncActor(() => requestSyncActor(api, timeoutMs));\n}\n\nasync function requestSyncActor(\n api: ApiClient,\n timeoutMs: number,\n): Promise<SyncActor | null> {\n try {\n const raw = await Promise.race([\n api.get<Record<string, unknown>>(\"/api/me\"),\n new Promise<never>((_, reject) => {\n const timer = setTimeout(\n () => reject(new Error(`timed out after ${timeoutMs}ms`)),\n timeoutMs,\n );\n // Don't keep the process alive just for the stamp timeout.\n timer.unref?.();\n }),\n ]);\n return actorFromMe(raw);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n console.warn(\n ` (skipping user-stamp on sync commit — couldn't fetch /api/me: ${reason})`,\n );\n return null;\n }\n}\n","import chalk from \"chalk\";\nimport { Command } from \"commander\";\nimport ora from \"ora\";\nimport prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { readThemeConfig, writeThemeConfig } from \"../theme-config.js\";\nimport { ThemeAssetManifest } from \"../theme/asset-manifest.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { PushConflictError, Syncer } from \"../theme/syncer.js\";\nimport { ShadowRepo } from \"../theme/shadow-repo.js\";\nimport {\n commitPushedState,\n conflictMarkerBlockLine,\n diffAgainstShadow,\n finalizeManifestOnlyPush,\n findUnresolvedConflicts,\n} from \"../theme/merge-push.js\";\nimport { seedBaselineFromServer } from \"../theme/auto-baseline.js\";\nimport { migrateLegacyChecksumsIntoShadow } from \"../theme/legacy-migration.js\";\nimport {\n fetchSyncActor,\n syncCommitSubject,\n type SyncActor,\n} from \"../theme/sync-identity.js\";\nimport type { ChangeEntry } from \"@fluid-app/fluid-cli\";\nimport { themes } from \"@fluid-app/themes-api-client\";\nimport {\n selectTheme,\n findTheme,\n type ApplicationTheme,\n} from \"../theme-picker.js\";\nimport { findWorkspace, resolveThemeRootFromCwd } from \"../workspace.js\";\nimport { formatError } from \"../theme/format-error.js\";\nimport { findLiquidBlockTagDiagnostics } from \"../theme/liquid-delimiters.js\";\n\nexport function createPushCommand(): Command {\n return new Command(\"push\")\n .description(\"Push local theme files to a remote theme\")\n .option(\"-t, --theme <name-or-id>\", \"Theme name or ID to push to\")\n .option(\"-n, --nodelete\", \"Do not delete remote files missing locally\")\n .option(\n \"-f, --force\",\n \"Skip local Liquid validation and the server-side merge check\",\n )\n .option(\"-p, --publish\", \"Publish the theme after pushing\")\n .option(\n \"-u, --unpublished\",\n \"Create a new unpublished theme and push to it\",\n )\n .option(\n \"--auto-baseline\",\n \"When no local baseline exists, record the server's current state \" +\n \"as the baseline and push only what differs locally (never \" +\n \"modifies local files, never deletes server-only files)\",\n )\n .option(\"--root <path>\", \"Theme root directory\", \".\")\n .action(\n async (opts: {\n theme?: string;\n nodelete?: boolean;\n force?: boolean;\n publish?: boolean;\n unpublished?: boolean;\n autoBaseline?: boolean;\n root: string;\n }) => {\n requireToken();\n\n let rootPath = opts.root;\n if (rootPath === \".\") {\n const workspace = findWorkspace();\n if (workspace) {\n rootPath = resolveThemeRootFromCwd(workspace) ?? rootPath;\n }\n }\n\n const themeRoot = new ThemeRoot(rootPath);\n if (!themeRoot.isValid()) {\n console.error(`'${rootPath}' does not look like a theme directory.`);\n process.exit(1);\n }\n\n const api = createApiClient();\n const config = readThemeConfig(themeRoot.root);\n let theme: ApplicationTheme;\n\n if (opts.unpublished) {\n const { name } = await prompts(\n {\n type: \"text\",\n name: \"name\",\n message: \"Name for the new theme\",\n },\n { onCancel: () => process.exit(130) },\n );\n if (!name) {\n console.error(\"Theme name is required.\");\n process.exit(1);\n }\n const body = await themes.createApplicationTheme(api, {\n application_theme: { name, status: \"draft\" },\n });\n theme = body.application_theme;\n console.log(\n `Created unpublished theme: ${theme.name} (#${theme.id})`,\n );\n } else if (opts.theme) {\n theme = await findTheme(api, opts.theme);\n } else if (config) {\n console.log(\n ` Using theme from .fluid-theme.json: ${chalk.bold(config.themeName)} (#${config.themeId})`,\n );\n const body = await themes.getApplicationTheme(api, config.themeId);\n theme = body.application_theme;\n } else {\n theme = await selectTheme(api, \"Select a theme to push to\");\n }\n\n const shadow = await ShadowRepo.open(themeRoot.root, theme.id);\n\n // Legacy checksum-era dirs (pre-shadow CLI) migrate here — on\n // push as well as pull, so a dir that never pulled with the\n // new CLI still recovers its merge base instead of dead-ending\n // on the baseline check below. Idempotent; the migration wins\n // over `--auto-baseline` server seeding because a migrated\n // HEAD holds the local \"unmodified since last sync\" blobs.\n await migrateLegacyChecksumsIntoShadow({\n shadow,\n themeRoot,\n absoluteRoot: themeRoot.root,\n themeId: theme.id,\n });\n\n const localFiles = themeRoot.files().filter((f) => f.exists);\n\n // Lazy, memoized identity lookup for commit stamping — only\n // fired when we actually commit, never blocks or fails a push.\n let actorPromise: Promise<SyncActor | null> | null = null;\n const getActor = (): Promise<SyncActor | null> =>\n (actorPromise ??= fetchSyncActor(api));\n\n // Refuse to push files that still contain `<<<<<<< / >>>>>>>`\n // markers from a previous merge — the whole point of writing\n // them was to hand resolution to the user; sending them\n // upstream would ship broken content. This guard is absolute:\n // `--auto-baseline` (non-interactive) refuses too, but emits a\n // machine-readable line the desktop can surface well.\n const unresolved = findUnresolvedConflicts(localFiles);\n if (unresolved.length > 0) {\n console.log();\n console.log(\n chalk.red(\n `✗ ${unresolved.length} file(s) still contain unresolved conflict markers:`,\n ),\n );\n for (const key of unresolved) console.log(` ${key}`);\n console.log();\n console.log(\n ` Edit each file to reconcile the ${chalk.cyan(\"<<<<<<<\")} / ${chalk.cyan(\">>>>>>>\")} sections,`,\n );\n console.log(` then re-run ${chalk.cyan(\"fluid theme push\")}.`);\n console.log();\n if (opts.autoBaseline) {\n console.error(conflictMarkerBlockLine(unresolved));\n }\n process.exit(1);\n }\n\n const syncer = new Syncer(api, theme.id, themeRoot);\n\n // baseSha — server-side merge check on every write. Skipped\n // when `--force`, when the config was written by an older CLI\n // that didn't yet capture the pull's `content_version_sha`,\n // or when we're pushing to a different theme than the one the\n // config was written for (a `--unpublished` create, or\n // `--theme` pointing somewhere else). `baseSha` is per-theme —\n // reusing one theme's sha as the base of another theme's push\n // always 409s.\n const configMatchesTheme = config?.themeId === theme.id;\n let baseSha =\n opts.force || !configMatchesTheme ? null : (config?.baseSha ?? null);\n\n // A baseline exists when either half is present: shadow HEAD\n // (the local diff base — written by pull, a legacy migration,\n // or a previous auto-baseline) or a stored `baseSha` (the\n // server-side race check). HEAD alone is enough to diff\n // honestly; a missing baseSha only skips the server preflight.\n const hasBaseline = baseSha != null || (await shadow.hasHead());\n\n // No baseline at all: without `--auto-baseline`, refuse —\n // otherwise the CLI silently clobbers whatever the server\n // currently has, with no way for the user to see the\n // divergence first. `--force` opts out (explicit clobber);\n // `--unpublished` skips because the theme was created empty\n // in this same command and has nothing to protect.\n if (!opts.force && !opts.unpublished && !hasBaseline) {\n if (!opts.autoBaseline) {\n console.error();\n console.error(\n chalk.red(\n `No local baseline for theme \"${theme.name}\" (#${theme.id}).`,\n ),\n );\n console.error();\n console.error(\n ` Run ${chalk.cyan(`fluid theme pull -t ${theme.id}`)} first to sync down the current server state,`,\n );\n console.error(\n ` then push — this way you see what would change before it goes live.`,\n );\n console.error();\n console.error(\n ` Or re-run with ${chalk.cyan(\"--auto-baseline\")} to record the server's current state`,\n );\n console.error(\n ` as the baseline and push only what differs locally (local files are never modified).`,\n );\n console.error();\n console.error(\n ` Or, if you know what you're doing and want to overwrite the server's current`,\n );\n console.error(\n ` contents wholesale, re-run with ${chalk.cyan(\"--force\")}.`,\n );\n console.error();\n process.exit(1);\n }\n\n // `--auto-baseline`: record the server's current state as\n // the baseline WITHOUT touching a single local file, then\n // fall through to the normal diff. Server-only files are\n // deliberately kept out of the baseline so they can never\n // be reported as local deletions (see auto-baseline.ts).\n const baselineSpinner = ora(\n `No local baseline — recording current server state for ${theme.name} (#${theme.id})…`,\n ).start();\n try {\n const resources = await syncer.downloadAll();\n const seedResult = await seedBaselineFromServer({\n shadow,\n themeRoot,\n remote: resources,\n fetchBinary: (url) => syncer.downloadBinaryAsset(url),\n message: syncCommitSubject(\n \"Baseline from server\",\n await getActor(),\n ),\n });\n baseSha = syncer.remoteSha() ?? null;\n const parts = [`recorded ${seedResult.recorded.length} file(s)`];\n if (seedResult.serverOnly.length > 0) {\n parts.push(\n `left ${seedResult.serverOnly.length} server-only file(s) untouched`,\n );\n }\n baselineSpinner.succeed(`Baseline recorded — ${parts.join(\", \")}.`);\n for (const err of seedResult.errors) {\n console.warn(` ${chalk.yellow(\"warn\")} ${err}`);\n }\n } catch (e) {\n baselineSpinner.fail(\n `Could not record a baseline from the server: ${formatError(e)}`,\n );\n process.exit(1);\n }\n }\n\n // Diff against shadow HEAD is the source of truth for \"what\n // changed since last sync\" — replaces the sha256 checksum map.\n const { changed, deleted } = await diffAgainstShadow(themeRoot, shadow);\n const managedAssetCount = new ThemeAssetManifest(themeRoot.root).keys()\n .length;\n\n if (\n changed.length === 0 &&\n deleted.length === 0 &&\n managedAssetCount === 0\n ) {\n console.log(\"Nothing to push — local matches the last synced state.\");\n await persistConfig();\n return;\n }\n\n const spinner = ora(`Pushing to ${theme.name} (#${theme.id})…`).start();\n\n // Validate before linking URL references so a schema failure cannot\n // mutate the target theme at all.\n if (!opts.force) {\n const validationErrors: string[] = [];\n for (const file of changed) {\n if (!file.isLiquid) continue;\n for (const diagnostic of file.validateSchema()) {\n if (diagnostic.severity === \"error\") {\n validationErrors.push(\n `${file.relativePath}: ${diagnostic.message}`,\n );\n }\n }\n for (const diagnostic of findLiquidBlockTagDiagnostics(\n file.read(),\n )) {\n validationErrors.push(\n `${file.relativePath}: ${diagnostic.message}`,\n );\n }\n }\n if (validationErrors.length > 0) {\n spinner.fail(\n `Liquid validation failed (${validationErrors.length} error(s)). Use --force to skip.`,\n );\n for (const error of validationErrors) console.error(` ${error}`);\n process.exit(1);\n }\n }\n try {\n await syncer.preflightPush(baseSha);\n baseSha = syncer.remoteSha() ?? baseSha;\n } catch (e) {\n if (e instanceof PushConflictError) {\n renderPullFirst(spinner);\n process.exit(1);\n }\n throw e;\n }\n\n let linked = 0;\n try {\n linked = await syncer.linkManagedAssets({ replace: true });\n // Reference writes are outside the ordinary resource PUT path. The\n // syncer refreshes its remote SHA afterwards so following PUTs keep\n // the existing Phase-003a conflict chain intact.\n baseSha = syncer.remoteSha() ?? baseSha;\n } catch (error) {\n spinner.fail(\n `Could not save remote asset references: ${formatError(error)}`,\n );\n process.exit(1);\n }\n\n if (changed.length === 0 && deleted.length === 0 && linked === 0) {\n try {\n await finalizeManifestOnlyPush(syncer, themeRoot, shadow);\n } catch (error) {\n spinner.fail(\n `Could not finalize remote asset references: ${formatError(error)}`,\n );\n process.exit(1);\n }\n spinner.succeed(\"Nothing to push — local matches the remote theme.\");\n await persistConfig();\n return;\n }\n\n let uploaded = 0;\n let deletedCount = 0;\n const errors: string[] = [];\n let progress = 0;\n const total = changed.length + (opts.nodelete ? 0 : deleted.length);\n\n for (const file of changed) {\n try {\n await syncer.uploadFile(file, baseSha);\n baseSha = syncer.remoteSha() ?? baseSha;\n uploaded++;\n } catch (e) {\n if (e instanceof PushConflictError) {\n spinner.stop();\n renderPullFirst(ora());\n process.exit(1);\n }\n errors.push(`Upload ${file.relativePath}: ${formatError(e)}`);\n }\n spinner.text = `Pushing ${++progress}/${total} files…`;\n }\n\n if (!opts.nodelete) {\n for (const key of deleted) {\n try {\n await syncer.deleteRemoteFile(key, baseSha);\n baseSha = syncer.remoteSha() ?? baseSha;\n deletedCount++;\n } catch (e) {\n if (e instanceof PushConflictError) {\n spinner.stop();\n renderPullFirst(ora());\n process.exit(1);\n }\n errors.push(`Delete ${key}: ${formatError(e)}`);\n }\n spinner.text = `Pushing ${++progress}/${total} files…`;\n }\n }\n\n if (errors.length === 0) {\n try {\n // The selected target now owns every manifest reference, so a\n // later deleted dev sandbox cannot force a byte upload again.\n syncer.repointManagedAssetsToCurrentTheme();\n } catch (error) {\n errors.push(`Save asset provenance: ${formatError(error)}`);\n }\n }\n\n // Every write is on the server; ask Fluid to commit them as one\n // version. The writes themselves only mark the theme changed — this\n // one call is what makes a 120-file push a single commit instead of\n // 120, and it is the reason the server no longer has to guess where\n // an operation ended.\n //\n // Sent even when some files failed: what did land is real, and Fluid\n // commits whatever state it now holds. Skipping it would leave that\n // work uncommitted until a sweep. Managed-asset reference writes are\n // server writes too, so a link-only push also commits.\n if (uploaded > 0 || deletedCount > 0 || linked > 0) {\n await syncer.requestSync();\n }\n\n if (errors.length) {\n spinner.warn(`Pushed with ${errors.length} error(s).`);\n for (const err of errors) console.error(` ${err}`);\n // The live theme is missing every rejected file, so this is a\n // failure. Mist's lifecycle retry runs `fluid theme push\n // --auto-baseline` and keys off the exit code, and every other\n // failure in this command already exits 1 — exiting 0 here\n // reported a publish that did not happen.\n //\n // exitCode rather than process.exit(): the command still has\n // output to flush, and an immediate exit can truncate it.\n process.exitCode = 1;\n } else {\n spinner.succeed(\n `Pushed ${uploaded} file(s), saved ${linked} remote asset reference(s)` +\n (deletedCount > 0\n ? `, deleted ${deletedCount} remote file(s).`\n : \".\"),\n );\n }\n\n // Roll shadow HEAD forward only when every write succeeded —\n // committing the working tree on a partial push would make\n // `diffAgainstShadow` treat the failed files as \"already\n // synced\" on the next push, silently swallowing the retry.\n // Leave HEAD stale on failure and let the next push re-diff\n // and re-attempt.\n if (errors.length === 0) {\n // Describe what the push carried rather than stamping every\n // entry \"Push from Fluid CLI\". `changed` is the uploaded set\n // and `deleted` the removed keys — exactly what landed, since\n // we only get here when no write failed.\n const pushedChanges: ChangeEntry[] = [\n ...changed.map((file) => ({\n status: \"modified\" as const,\n path: file.relativePath,\n })),\n ...(opts.nodelete\n ? []\n : deleted.map((key) => ({\n status: \"deleted\" as const,\n path: key,\n }))),\n ];\n await commitPushedState(\n themeRoot,\n shadow,\n syncCommitSubject(\n \"Push\",\n await getActor(),\n new Date(),\n pushedChanges,\n ),\n );\n }\n\n await persistConfig();\n\n /**\n * Persist `.fluid-theme.json`. When a config already exists,\n * this is the pre-existing baseSha refresh. When it doesn't\n * (a scaffold's first `--auto-baseline` push), bind the dir\n * to the theme now — best-effort, since the company subdomain\n * needs one more API call — so the next push/pull needs no\n * interactive picker.\n */\n async function persistConfig(): Promise<void> {\n if (config) {\n writeThemeConfig(themeRoot.root, {\n themeId: theme.id,\n themeName: theme.name,\n company: config.company,\n baseSha: baseSha ?? undefined,\n assetManifestSha: new ThemeAssetManifest(\n themeRoot.root,\n ).fingerprint({ excludePending: true }),\n });\n return;\n }\n if (!opts.autoBaseline) return;\n try {\n const res = await api.get<{\n data?: { company?: { subdomain?: string } };\n }>(\"/api/company/v1/companies/me\");\n const subdomain = res.data?.company?.subdomain;\n if (!subdomain) return;\n writeThemeConfig(themeRoot.root, {\n themeId: theme.id,\n themeName: theme.name,\n company: subdomain,\n baseSha: baseSha ?? undefined,\n assetManifestSha: new ThemeAssetManifest(\n themeRoot.root,\n ).fingerprint({ excludePending: true }),\n });\n } catch {\n // Best-effort — the shadow baseline alone is enough for\n // the next push to diff correctly.\n }\n }\n\n if (opts.publish) {\n const pubSpinner = ora(\"Publishing theme…\").start();\n try {\n await themes.publishApplicationTheme(api, theme.id);\n pubSpinner.succeed(\"Theme published.\");\n } catch (e) {\n pubSpinner.fail(`Publish failed: ${e}`);\n // Content uploaded but the theme is still a draft. Without\n // this the caller is told the publish succeeded.\n process.exitCode = 1;\n }\n }\n },\n );\n}\n\nfunction renderPullFirst(spinner: ReturnType<typeof ora>): void {\n spinner.fail(\"Server has changed since your last pull. Push aborted.\");\n console.log();\n console.log(\n ` ${chalk.cyan(\"Run `fluid theme pull` first\")} to merge the remote changes,`,\n );\n console.log(\n ` then re-run push. Use ${chalk.cyan(\"fluid theme push --force\")} to overwrite anyway.`,\n );\n console.log();\n}\n","import { unlinkSync } from \"node:fs\";\nimport { sep } from \"node:path\";\nimport type { components } from \"@fluid-app/themes-api-client\";\nimport {\n MANAGED_ASSET_SHADOW_SENTINEL,\n ThemeAssetManifest,\n} from \"./asset-manifest.js\";\nimport { ShadowRepo, looksBinary, readIfExists } from \"./shadow-repo.js\";\nimport type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport { isStylesheetKey } from \"./stylesheet-keys.js\";\nimport { commitPushedState } from \"./merge-push.js\";\nimport { syncCommitSubject, type SyncActor } from \"./sync-identity.js\";\n\ntype RemoteResource = components[\"schemas\"][\"ApplicationThemeResource\"];\n\n/** `write(..., RESOLVES_CONFLICT)`: this write is the chosen side of a conflict. */\nconst RESOLVES_CONFLICT = true;\n\n/**\n * Appended to every per-file write failure. Resource keys are verbatim\n * relative paths, so a template named `Sale 11/14/2025, 12:30 PM` asks\n * the filesystem for directories that Windows (and any path already\n * occupied by a file) refuses. Renaming the template in the admin is\n * the only fix the user owns.\n */\nconst RENAME_REMEDY =\n 'Rename the template in the admin visual builder to remove / \\\\ : * ? \" < > | from its name, then pull again.';\n\nexport interface MergePullResult {\n /** Files written from the remote without any local content to merge. */\n written: number;\n /** Files that went through a clean three-way merge with no markers. */\n merged: number;\n /** Files where markers remain — the caller must surface these. */\n conflicts: string[];\n /** Conflicting files auto-resolved to one side via `resolve`. */\n autoResolved: string[];\n /** Files removed locally because remote no longer emitted them. */\n deleted: number;\n /** Files that needed no change (already in sync). */\n skipped: number;\n /** Non-fatal per-file errors. */\n errors: string[];\n}\n\nexport type FetchBinary = (url: string) => Promise<Buffer>;\n\nexport interface MergePullInput {\n themeRoot: ThemeRoot;\n shadow: ShadowRepo;\n remote: RemoteResource[];\n fetchBinary: FetchBinary;\n delete: boolean;\n /**\n * When true, skip the merge and take remote wholesale for every\n * file. Used by `fluid theme pull --force` when the user wants a\n * clean slate. HEAD still advances to reflect the state we wrote.\n */\n force?: boolean;\n /**\n * Remote resources already represented by `.fluid-assets.json`. They are\n * intentionally not materialized; ShadowRepo retains only a tiny path\n * sentinel, never their binary bytes.\n */\n skipRemoteKeys?: ReadonlySet<string>;\n /**\n * Non-interactive conflict resolution (`fluid theme pull --resolve\n * <local|remote>`): conflicting hunks are resolved to the chosen\n * side via `git merge-file --ours/--theirs` instead of writing\n * `<<<<<<<` markers; binary conflicts keep the whole chosen side.\n * Because \"remote\" resolution discards local hunks that live\n * NOWHERE else (shadow HEAD only holds the last-synced base, not\n * the user's edits), the pre-merge working tree is committed to the\n * shadow repo first — so the discarded state stays restorable.\n * Undefined = current behavior (markers).\n */\n resolve?: \"local\" | \"remote\";\n /**\n * Signed-in user for commit stamping (best-effort — null falls back\n * to the legacy machine-flavored subjects).\n */\n actor?: SyncActor | null;\n onProgress?: (done: number, total: number) => void;\n}\n\n/**\n * Reconcile the just-downloaded remote tree against the working tree,\n * using the shadow repo's HEAD as the merge base. Text files that\n * diverge on both sides get run through `git merge-file`; the result\n * — clean or with `<<<<<<<` markers — is written to disk. Binary\n * files fall back to \"take remote\" because there is no principled\n * three-way merge for them.\n *\n * HEAD advances to reflect the remote state we just materialized,\n * even when unresolved conflict markers remain in the working tree.\n * The user resolves markers in their editor and re-runs push; push\n * refuses to send files whose content still starts with a marker,\n * so a half-resolved push cannot silently ship broken content.\n */\nexport async function mergePull(\n input: MergePullInput,\n): Promise<MergePullResult> {\n const { themeRoot, shadow, remote, fetchBinary, onProgress } = input;\n const doDelete = input.delete;\n const result: MergePullResult = {\n written: 0,\n merged: 0,\n conflicts: [],\n autoResolved: [],\n deleted: 0,\n skipped: 0,\n errors: [],\n };\n\n // In `resolve` mode, working-tree writes are DEFERRED until after\n // the merge loop: if any conflict got auto-resolved, the pre-merge\n // working tree (still untouched at that point) is committed to the\n // shadow repo first, so the side we discard remains restorable.\n const pendingWrites: Array<{\n key: string;\n file: ThemeFile;\n content: Buffer;\n record: () => void;\n resolvesConflict: boolean;\n }> = [];\n // Keys whose bytes never reached disk — refused by the traversal\n // guard, or failed on the write. They must be kept out of the shadow\n // HEAD commit below, exactly like a failed download: HEAD is what\n // push diffs against, so recording content we did not write would\n // turn a tolerated failure into a remote DELETE. Refused keys have a\n // second reason — git rejects paths like `../x` outright, and the\n // commit takes the whole pull down with it.\n const unwrittenKeys = new Set<string>();\n const tryWrite = (key: string, file: ThemeFile, content: Buffer): boolean => {\n try {\n file.write(content);\n return true;\n } catch (e) {\n unwrittenKeys.add(key);\n result.errors.push(`Reconcile ${key}: ${errMsg(e)}. ${RENAME_REMEDY}`);\n return false;\n }\n };\n /**\n * Write `content`, then apply `record` — the bookkeeping that says\n * which bucket of the summary this file counted as. The two travel\n * together, including into the deferred flush: bookkeeping applied\n * when the write was merely QUEUED would report a file the flush\n * never managed to write as both written and errored.\n */\n const write = (\n key: string,\n file: ThemeFile,\n content: Buffer,\n record: () => void,\n resolvesConflict = false,\n ): void => {\n if (input.resolve) {\n pendingWrites.push({ key, file, content, record, resolvesConflict });\n return;\n }\n if (tryWrite(key, file, content)) record();\n };\n\n const remoteContent = new Map<string, Buffer>();\n const remoteKeys = new Set<string>();\n let done = 0;\n for (const resource of remote) {\n remoteKeys.add(resource.key);\n if (input.skipRemoteKeys?.has(resource.key)) {\n onProgress?.(++done, remote.length);\n continue;\n }\n try {\n const buf = await materialize(resource, fetchBinary);\n if (buf) remoteContent.set(resource.key, buf);\n } catch (e) {\n result.errors.push(`Download ${resource.key}: ${errMsg(e)}`);\n }\n onProgress?.(++done, remote.length);\n }\n\n for (const [key, remoteBuf] of remoteContent) {\n const file = themeRoot.file(key);\n\n if (!file.absolutePath.startsWith(themeRoot.root + sep)) {\n result.errors.push(`Reconcile ${key}: path traversal detected`);\n unwrittenKeys.add(key);\n continue;\n }\n\n if (input.force) {\n // `--force`: clobber everything with the remote content. HEAD\n // still advances below so the next pull's merge base is right.\n write(key, file, remoteBuf, () => result.written++);\n continue;\n }\n\n const localBuf = readIfExists(file.absolutePath);\n const baseBuf = await shadow.blobAtHead(key);\n\n if (localBuf == null) {\n write(key, file, remoteBuf, () => result.written++);\n continue;\n }\n\n if (localBuf.equals(remoteBuf)) {\n result.skipped++;\n continue;\n }\n\n if (baseBuf && localBuf.equals(baseBuf)) {\n write(key, file, remoteBuf, () => result.written++);\n continue;\n }\n\n if (baseBuf && remoteBuf.equals(baseBuf)) {\n // A prior unresolved pull advances shadow HEAD to the clean remote\n // state while leaving markers in the working tree. A later\n // `--resolve remote` must be able to finish that interrupted clone;\n // treating the marker-filled file as an ordinary local edit traps\n // every retry in the same broken state.\n if (\n input.resolve === \"remote\" &&\n !looksBinary(localBuf) &&\n hasGeneratedConflictMarkers(localBuf)\n ) {\n write(\n key,\n file,\n remoteBuf,\n () => result.autoResolved.push(`${key} (cleared conflict markers)`),\n RESOLVES_CONFLICT,\n );\n continue;\n }\n // Server didn't move; keep the user's local edits.\n result.skipped++;\n continue;\n }\n\n const isBinary =\n looksBinary(localBuf) ||\n looksBinary(remoteBuf) ||\n (baseBuf ? looksBinary(baseBuf) : false);\n\n if (isBinary) {\n // No textual merge for binary files: whole-file either/or.\n if (input.resolve === \"local\") {\n // Keep the local side untouched — nothing to write.\n result.autoResolved.push(`${key} (binary — kept local)`);\n continue;\n }\n // Take remote. In `--resolve remote` that's the chosen side;\n // otherwise flag it so the user can compare against the shadow\n // repo (`git show HEAD:...`) and reapply local intent by hand.\n write(\n key,\n file,\n remoteBuf,\n () => {\n if (input.resolve === \"remote\") {\n result.autoResolved.push(`${key} (binary — kept remote)`);\n } else {\n result.conflicts.push(`${key} (binary — kept remote)`);\n }\n },\n input.resolve === \"remote\",\n );\n continue;\n }\n\n // First merge WITHOUT a favor side so we know whether this file\n // actually conflicts — that drives honest bookkeeping (merged vs\n // autoResolved) and the pre-merge snapshot decision.\n const { merged, hasConflicts } = await shadow.merge3(\n baseBuf,\n localBuf,\n remoteBuf,\n );\n if (hasConflicts && input.resolve) {\n const favored = await shadow.merge3(\n baseBuf,\n localBuf,\n remoteBuf,\n input.resolve,\n );\n write(\n key,\n file,\n favored.merged,\n () => result.autoResolved.push(key),\n RESOLVES_CONFLICT,\n );\n continue;\n }\n write(key, file, merged, () => {\n if (hasConflicts) result.conflicts.push(key);\n else result.merged++;\n });\n }\n\n // Flush deferred writes (resolve mode only). When any conflict was\n // auto-resolved, snapshot the pre-merge working tree first — it is\n // the ONLY place the losing side of each resolution still exists.\n if (input.resolve) {\n if (pendingWrites.some((w) => w.resolvesConflict)) {\n await commitPushedState(\n themeRoot,\n shadow,\n syncCommitSubject(\"Snapshot before pull\", input.actor ?? null),\n );\n }\n for (const { key, file, content, record } of pendingWrites) {\n if (tryWrite(key, file, content)) record();\n }\n }\n\n if (doDelete && (await shadow.hasHead())) {\n for (const file of themeRoot.files()) {\n if (remoteKeys.has(file.relativePath)) continue;\n // Stylesheet-hidden files are intentionally absent from the\n // resources index — deleting them locally would fight the\n // STYLESHEET_STRICT_INPUT feature, not honor a real deletion.\n if (isStylesheetKey(file.relativePath)) continue;\n\n const baseBuf = await shadow.blobAtHead(file.relativePath);\n if (!baseBuf) continue; // never seen by shadow — leave alone\n const localBuf = readIfExists(file.absolutePath);\n if (!localBuf) continue;\n if (!localBuf.equals(baseBuf)) continue; // user has local edits\n\n try {\n unlinkSync(file.absolutePath);\n result.deleted++;\n } catch {\n // ignore best-effort deletion\n }\n }\n }\n\n // Advance HEAD to the remote state we just downloaded. Even when\n // some files carry unresolved conflict markers, HEAD reflects the\n // server's clean bytes — the next push's diff-against-HEAD is what\n // surfaces the markers as \"unresolved\".\n //\n // Preserve failed-download paths from the previous HEAD so a\n // transient network error doesn't quietly drop them from the merge\n // base. If a file was in HEAD before the pull and its download\n // just failed, the next pull needs the previous blob as the base\n // for its merge — otherwise `blobAtHead` returns null next time and\n // a server-side change fires a spurious null-base conflict on a\n // file the user never touched.\n //\n // Keys whose write failed take the same route: their bytes are on\n // the server but not on disk, so recording the remote content here\n // would claim a state this checkout never had.\n const commitEntries: Array<{ path: string; sha: string }> = [];\n for (const [key, buf] of remoteContent) {\n if (unwrittenKeys.has(key)) continue;\n commitEntries.push({ path: key, sha: await shadow.writeBlob(buf) });\n }\n for (const key of remoteKeys) {\n if (input.skipRemoteKeys?.has(key)) continue;\n // Downloaded AND written — already added above.\n if (remoteContent.has(key) && !unwrittenKeys.has(key)) continue;\n const prevBlob = await shadow.blobAtHead(key);\n if (prevBlob == null) continue; // wasn't in HEAD to begin with\n commitEntries.push({ path: key, sha: await shadow.writeBlob(prevBlob) });\n }\n const assetManifest = new ThemeAssetManifest(themeRoot.root);\n let managedAssetSentinelSha: string | undefined;\n for (const key of input.skipRemoteKeys ?? []) {\n // Ignored resources without a manifest link remain outside the shadow\n // entirely. Otherwise removing an ignore rule could make a skipped file\n // look like a local deletion on the next push.\n if (!remoteKeys.has(key) || !assetManifest.has(key)) continue;\n managedAssetSentinelSha ??= await shadow.writeBlob(\n MANAGED_ASSET_SHADOW_SENTINEL,\n );\n commitEntries.push({\n path: key,\n sha: managedAssetSentinelSha,\n });\n }\n // A pull containing only manifest-backed assets still needs a shadow commit:\n // sentinel entries replace any legacy binary blobs while keeping a baseline\n // for an intentional manifest-entry removal on a later push.\n // When the server becomes empty, record an empty tree rather than retaining\n // stale paths from HEAD as a future merge/delete baseline — but only when\n // the server really is empty. Empty entries with a non-empty remote mean\n // every write failed, and HEAD must survive as the next pull's merge base.\n if (\n commitEntries.length > 0 ||\n (remoteKeys.size === 0 && (await shadow.hasHead()))\n ) {\n await shadow.commitState(\n commitEntries,\n syncCommitSubject(\"Pull\", input.actor ?? null),\n );\n }\n\n return result;\n}\n\nfunction hasGeneratedConflictMarkers(content: Buffer): boolean {\n const text = content.toString(\"utf8\");\n return (\n /^<<<<<<< local\\r?$/m.test(text) &&\n /^=======\\r?$/m.test(text) &&\n /^>>>>>>> remote\\r?$/m.test(text)\n );\n}\n\nasync function materialize(\n resource: RemoteResource,\n fetchBinary: FetchBinary,\n): Promise<Buffer | null> {\n if (resource.resource_type === \"FileResource\" && resource.url) {\n return fetchBinary(resource.url);\n }\n if (resource.content == null) return null;\n const text =\n typeof resource.content === \"string\"\n ? resource.content\n : JSON.stringify(resource.content);\n return Buffer.from(text);\n}\n\nfunction errMsg(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n","import { join, resolve } from \"node:path\";\nimport chalk from \"chalk\";\nimport { Command } from \"commander\";\nimport ora from \"ora\";\nimport prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { readThemeConfig, writeThemeConfig } from \"../theme-config.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { Syncer } from \"../theme/syncer.js\";\nimport { ShadowRepo } from \"../theme/shadow-repo.js\";\nimport { mergePull } from \"../theme/merge-pull.js\";\nimport { migrateLegacyChecksumsIntoShadow } from \"../theme/legacy-migration.js\";\nimport { fetchSyncActor } from \"../theme/sync-identity.js\";\nimport { selectTheme, findTheme } from \"../theme-picker.js\";\nimport { findWorkspace, resolveThemeRootFromCwd } from \"../workspace.js\";\nimport { ThemeAssetManifest } from \"../theme/asset-manifest.js\";\nimport { CaseCollisionError } from \"../theme/case-collisions.js\";\n\ninterface CompanyMe {\n data: { company: { subdomain?: string; name?: string } };\n}\n\nasync function fetchCompanySubdomain(\n api: ReturnType<typeof createApiClient>,\n): Promise<string> {\n const res = await api.get<CompanyMe>(\"/api/company/v1/companies/me\");\n const subdomain = res.data?.company?.subdomain;\n if (!subdomain) {\n console.error(\n \"Could not determine company subdomain. Make sure your token is valid.\",\n );\n process.exit(1);\n }\n return subdomain;\n}\n\nexport function createPullCommand(): Command {\n return new Command(\"pull\")\n .description(\"Pull a remote theme to your local directory\")\n .option(\"-t, --theme <name-or-id>\", \"Theme name or ID to pull\")\n .option(\"-n, --nodelete\", \"Do not delete local files missing on remote\")\n .option(\"--root <path>\", \"Theme root directory\")\n .option(\"-y, --yes\", \"Skip confirmation prompt\")\n .option(\n \"-f, --force\",\n \"Overwrite local without merging (skip conflict markers)\",\n )\n .option(\n \"--resolve <side>\",\n \"Auto-resolve merge conflicts to one side instead of writing \" +\n \"conflict markers: 'local' keeps your files' hunks, 'remote' \" +\n \"takes the server's (for non-interactive use)\",\n )\n .action(\n async (opts: {\n theme?: string;\n nodelete?: boolean;\n root?: string;\n yes?: boolean;\n force?: boolean;\n resolve?: string;\n }) => {\n requireToken();\n\n if (\n opts.resolve !== undefined &&\n opts.resolve !== \"local\" &&\n opts.resolve !== \"remote\"\n ) {\n console.error(\n `Invalid --resolve value \"${opts.resolve}\" — use \"local\" or \"remote\".`,\n );\n process.exit(1);\n }\n const resolveSide = opts.resolve as \"local\" | \"remote\" | undefined;\n\n const api = createApiClient();\n const workspace = findWorkspace();\n\n const theme = opts.theme\n ? await findTheme(api, opts.theme)\n : await selectTheme(api, \"Select a theme to pull\");\n\n const subdomain = await fetchCompanySubdomain(api);\n let root: string;\n if (opts.root) {\n root = opts.root;\n } else if (workspace) {\n root =\n resolveThemeRootFromCwd(workspace) ??\n join(workspace.root, \"local\", subdomain);\n } else {\n root = `.`;\n }\n\n const absoluteRoot = resolve(root);\n const existingConfig = readThemeConfig(absoluteRoot);\n\n console.log();\n console.log(` Theme: ${chalk.bold(theme.name)} (#${theme.id})`);\n console.log(` Company: ${chalk.bold(subdomain)}`);\n console.log(` Target: ${chalk.bold(absoluteRoot)}`);\n console.log();\n\n if (!opts.yes) {\n const { confirmed } = await prompts(\n {\n type: \"confirm\",\n name: \"confirmed\",\n message: \"Pull theme to this directory?\",\n initial: true,\n },\n { onCancel: () => process.exit(130) },\n );\n if (!confirmed) {\n console.log(\"Aborted.\");\n process.exit(0);\n }\n }\n\n const themeRoot = new ThemeRoot(root);\n const shadow = await ShadowRepo.open(absoluteRoot, theme.id);\n await migrateLegacyChecksumsIntoShadow({\n shadow,\n themeRoot,\n absoluteRoot,\n themeId: theme.id,\n });\n const syncer = new Syncer(api, theme.id, themeRoot);\n\n // Fire the identity lookup alongside the download — it never\n // rejects, and mergePull only awaits it at commit time.\n const actorPromise = fetchSyncActor(api);\n\n const spinner = ora(`Pulling ${theme.name} (#${theme.id})…`).start();\n const resources = await syncer.downloadAll().catch((error: unknown) => {\n if (!(error instanceof CaseCollisionError)) throw error;\n spinner.fail(error.message);\n process.exit(1);\n });\n const externalizedAssets = await syncer.externalizePulledAssets(\n resources,\n {\n delete: !opts.nodelete,\n },\n );\n\n const result = await mergePull({\n themeRoot,\n shadow,\n remote: resources,\n fetchBinary: (url) => syncer.downloadBinaryAsset(url),\n delete: !opts.nodelete,\n force: opts.force ?? false,\n skipRemoteKeys: externalizedAssets.managedKeys,\n resolve: resolveSide,\n actor: await actorPromise,\n onProgress: (done, total) => {\n spinner.text = `Downloading ${done}/${total} files…`;\n },\n });\n result.errors.push(...externalizedAssets.errors);\n\n const parts: string[] = [];\n if (result.written > 0) parts.push(`wrote ${result.written} file(s)`);\n if (result.merged > 0)\n parts.push(`merged ${result.merged} file(s) cleanly`);\n if (externalizedAssets.linked > 0) {\n parts.push(\n `kept ${externalizedAssets.linked} binary asset(s) remote`,\n );\n }\n if (result.autoResolved.length > 0)\n parts.push(\n `auto-resolved ${result.autoResolved.length} conflict(s) (kept ${resolveSide})`,\n );\n if (result.deleted > 0)\n parts.push(`deleted ${result.deleted} local file(s)`);\n if (result.skipped > 0) parts.push(`${result.skipped} already in sync`);\n\n if (result.errors.length) {\n spinner.warn(\n `Pulled with ${result.errors.length} error(s): ${parts.join(\", \")}.`,\n );\n for (const e of result.errors) console.error(` ${e}`);\n // Every errored file is one the checkout did NOT receive, so the\n // local tree does not match the server. Conflicts already exit 1\n // below; this branch used to fall through at 0.\n //\n // Mist spawns `fluid theme pull -t <id> --root <path> --yes` to\n // clone a theme, and only records a lifecycle failure when the\n // exit code is non-zero — so a partial clone never reached the\n // agent's context and it went on to edit a checkout that was\n // missing files.\n //\n // exitCode rather than an immediate exit: the per-file error\n // list is printed right here and `.fluid-theme.json` is written\n // below, both of which still need to happen.\n process.exitCode = 1;\n } else if (result.conflicts.length > 0) {\n spinner.warn(\n `${result.conflicts.length} conflict(s) — resolve markers before pushing: ${parts.join(\", \")}.`,\n );\n console.log();\n for (const c of result.conflicts) {\n console.log(` ${chalk.yellow(\"CONFLICT\")} ${c}`);\n }\n console.log();\n console.log(\n ` Edit each file above to reconcile the ${chalk.cyan(\"<<<<<<<\")} / ${chalk.cyan(\">>>>>>>\")} markers,`,\n );\n console.log(\n ` then run ${chalk.cyan(\"fluid theme push\")} once your resolution is in place.`,\n );\n console.log();\n } else {\n spinner.succeed(parts.join(\", \") || \"Already up to date.\");\n }\n\n // Update .fluid-theme.json — baseSha is the server-side handle\n // the push preflight uses; the merge base itself lives in the\n // shadow repo.\n const remoteSha = syncer.remoteSha();\n writeThemeConfig(absoluteRoot, {\n themeId: theme.id,\n themeName: theme.name,\n company: subdomain,\n baseSha: remoteSha ?? existingConfig?.baseSha,\n assetManifestSha: new ThemeAssetManifest(absoluteRoot).fingerprint({\n excludePending: true,\n }),\n });\n\n if (result.conflicts.length > 0) process.exit(1);\n },\n );\n}\n","import chalk from \"chalk\";\nimport { Command } from \"commander\";\nimport {\n findMissingSectionReferences,\n validateSchemaText,\n VALID_SETTING_TYPES,\n type BlocksSchemaType,\n type Diagnostic,\n type TemplateInput,\n} from \"@fluid-app/theme-schema\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { findLiquidBlockTagDiagnostics } from \"../theme/liquid-delimiters.js\";\nimport { findWorkspace, resolveThemeRootFromCwd } from \"../workspace.js\";\n\ninterface FileDiagnostics {\n path: string;\n diagnostics: Diagnostic[];\n}\n\n// A theme section is defined by a liquid file under the top-level `sections/`\n// directory. Returns the section name a `{% section %}` tag would reference, or\n// null if the file is not a section definition. Handles both the flat layout\n// (`sections/hero.liquid`) and the nested one (`sections/hero/index.liquid`).\nfunction sectionNameOf(relativePath: string): string | null {\n const parts = relativePath.split(/[/\\\\]/);\n if (parts[0] === \"sections\" && parts.length >= 2) {\n return parts[1]!.replace(/\\.liquid$/, \"\");\n }\n return null;\n}\n\nexport function createLintCommand(): Command {\n return new Command(\"lint\")\n .description(\"Validate theme files locally (read-only — no upload)\")\n .option(\"--root <path>\", \"Theme root directory\", \".\")\n .option(\"--json\", \"Output results as compact JSON\")\n .action(async (opts: { root: string; json?: boolean }) => {\n // Resolve the theme root the same way push/dev do: when left at the\n // default, prefer the workspace's theme root if we're inside one.\n let rootPath = opts.root;\n if (rootPath === \".\") {\n const workspace = findWorkspace();\n if (workspace) {\n rootPath = resolveThemeRootFromCwd(workspace) ?? rootPath;\n }\n }\n\n const themeRoot = new ThemeRoot(rootPath);\n if (!themeRoot.isValid()) {\n const message = `'${rootPath}' does not look like a theme directory.`;\n if (opts.json) {\n console.log(JSON.stringify({ ok: false, error: message }));\n } else {\n console.error(message);\n }\n process.exit(1);\n }\n\n const files = themeRoot.files();\n // Read each liquid file once and reuse the content for both passes\n // (validateSchemaText and the section scan) to avoid a double disk read.\n const liquidFiles = files\n .filter((f) => f.isLiquid)\n .map((f) => ({ file: f, content: f.read() }));\n\n const byFile = new Map<string, Diagnostic[]>();\n const record = (path: string, diagnostic: Diagnostic): void => {\n const existing = byFile.get(path);\n if (existing) existing.push(diagnostic);\n else byFile.set(path, [diagnostic]);\n };\n\n // ── Schema pass — the same {% schema %} validation `fluid theme push`\n // runs. blocksSchemaType mirrors ThemeFile.validateSchema: page/layout\n // templates use object blocks, sections use array blocks.\n for (const { file, content } of liquidFiles) {\n const blocksSchemaType: BlocksSchemaType = file.isTemplate\n ? \"object\"\n : \"array\";\n for (const diagnostic of validateSchemaText(content, {\n blocksSchemaType,\n })) {\n record(file.relativePath, diagnostic);\n }\n for (const diagnostic of findLiquidBlockTagDiagnostics(content)) {\n record(file.relativePath, diagnostic);\n }\n }\n\n // ── Section pass — flag `{% section 'x' %}` references to a section\n // that has no definition on disk. Section definitions and assets are\n // not themselves referrers, so they are excluded from the scan.\n const existingSectionNames = new Set<string>();\n for (const { file } of liquidFiles) {\n const name = sectionNameOf(file.relativePath);\n if (name) existingSectionNames.add(name);\n }\n const referrers: TemplateInput[] = liquidFiles\n .filter(({ file }) => sectionNameOf(file.relativePath) === null)\n .map(({ file, content }) => ({ path: file.relativePath, content }));\n for (const missing of findMissingSectionReferences(\n referrers,\n existingSectionNames,\n )) {\n record(missing.templatePath, missing.diagnostic);\n }\n\n const results: FileDiagnostics[] = [...byFile.entries()]\n .map(([path, diagnostics]) => ({ path, diagnostics }))\n .sort((a, b) => a.path.localeCompare(b.path));\n\n let errors = 0;\n let warnings = 0;\n for (const { diagnostics } of results) {\n for (const d of diagnostics) {\n if (d.severity === \"error\") errors++;\n else warnings++;\n }\n }\n\n // Surface the canonical setting types once (not in every diagnostic) so a\n // consumer fixing an \"Invalid settings type\" error has the valid set to\n // hand without it bloating each message.\n const hasInvalidSettingType = results.some(({ diagnostics }) =>\n diagnostics.some(\n (d) =>\n d.target?.kind === \"setting\" &&\n d.target.field === \"type\" &&\n d.target.settingType !== undefined,\n ),\n );\n\n if (opts.json) {\n console.log(\n JSON.stringify({\n ok: errors === 0,\n errors,\n warnings,\n filesChecked: liquidFiles.length,\n ...(hasInvalidSettingType\n ? { validSettingTypes: VALID_SETTING_TYPES }\n : {}),\n files: results,\n }),\n );\n } else {\n printText(results, errors, warnings, liquidFiles.length);\n }\n\n process.exit(errors > 0 ? 1 : 0);\n });\n}\n\nfunction plural(count: number, noun: string): string {\n return `${count} ${noun}${count === 1 ? \"\" : \"s\"}`;\n}\n\nfunction printText(\n results: FileDiagnostics[],\n errors: number,\n warnings: number,\n filesChecked: number,\n): void {\n for (const { path, diagnostics } of results) {\n console.log(chalk.bold(path));\n for (const d of diagnostics) {\n const label =\n d.severity === \"error\"\n ? chalk.red(\"error\".padEnd(7))\n : chalk.yellow(\"warning\".padEnd(7));\n // Only the first line — a few messages (e.g. the `Invalid JSON:` parse\n // error) carry a multi-line body that `--json` preserves in full.\n const message = d.message.split(\"\\n\")[0];\n console.log(` ${label} ${message}`);\n }\n }\n\n const suffix = `(${plural(filesChecked, \"file\")} checked)`;\n if (errors > 0) {\n console.log(\n `\\n${chalk.red(`✖ ${plural(errors, \"error\")}, ${plural(warnings, \"warning\")}`)} ${suffix}`,\n );\n } else if (warnings > 0) {\n console.log(\n `\\n${chalk.yellow(`⚠ ${plural(warnings, \"warning\")}`)} ${suffix}`,\n );\n } else {\n console.log(`${chalk.green(\"✓ No problems found\")} ${suffix}`);\n }\n}\n","import { Command } from \"commander\";\nimport { execFileSync } from \"node:child_process\";\nimport { rmSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport prompts from \"prompts\";\n\nconst DEFAULT_CLONE_URL = \"git@github.com:fluid-commerce/base-theme.git\";\n\nconst SAFE_NAME_RE = /^[a-zA-Z0-9_][a-zA-Z0-9._-]*$/;\n\nexport function createInitCommand(): Command {\n return new Command(\"init\")\n .description(\"Initialize a new theme by cloning the base theme\")\n .argument(\"[name]\", \"Directory name for the new theme\")\n .option(\"-u, --clone-url <url>\", \"Git URL to clone from\", DEFAULT_CLONE_URL)\n .action(async (name: string | undefined, opts: { cloneUrl: string }) => {\n if (!name) {\n const res = await prompts(\n {\n type: \"text\",\n name: \"name\",\n message: \"Theme name\",\n },\n { onCancel: () => process.exit(130) },\n );\n name = res.name as string;\n if (!name) {\n console.error(\"No name provided.\");\n process.exit(1);\n }\n }\n\n if (!SAFE_NAME_RE.test(name)) {\n console.error(\n `Invalid theme name: '${name}'. Use only letters, numbers, hyphens, underscores, and dots.`,\n );\n process.exit(1);\n }\n\n console.log(`Cloning theme from ${opts.cloneUrl} into ${name}…`);\n execFileSync(\"git\", [\"clone\", opts.cloneUrl, name], { stdio: \"inherit\" });\n\n for (const dir of [\".git\", \".github\"]) {\n const path = join(name, dir);\n if (existsSync(path)) rmSync(path, { recursive: true, force: true });\n }\n\n console.log(`\\nTheme initialized in ./${name}`);\n console.log(`Next steps:\\n cd ${name}\\n fluid theme push`);\n });\n}\n","import { Command } from \"commander\";\nimport prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { getLastDevThemeId } from \"../plugin-state.js\";\nimport { themes } from \"@fluid-app/themes-api-client\";\n\nfunction localSuggest(\n input: string,\n choices: prompts.Choice[],\n): prompts.Choice[] {\n if (!input) return choices;\n const lower = input.toLowerCase();\n return choices.filter((c) => c.title.toLowerCase().includes(lower));\n}\n\ninterface ThemeTemplate {\n id: number;\n name: string;\n themeable_type: string;\n default: boolean;\n}\n\ninterface TemplatesResponse {\n templates: ThemeTemplate[];\n}\n\nconst THEMEABLE_TYPE_MAP: Record<string, string> = {\n \"/home\": \"home_page\",\n \"/home/shop\": \"shop_page\",\n \"/home/join\": \"join_page\",\n \"/cart\": \"cart_page\",\n \"/home/blog\": \"post_page\",\n \"/home/categories\": \"category_page\",\n \"/home/collections\": \"collection_page\",\n};\n\nconst STATIC_ROUTES = [\n { label: \"Home\", path: \"/home\" },\n { label: \"Shop\", path: \"/home/shop\" },\n { label: \"Join / Sign Up\", path: \"/home/join\" },\n { label: \"Cart\", path: \"/cart\" },\n { label: \"Blog\", path: \"/home/blog\" },\n { label: \"Categories (all)\", path: \"/home/categories\" },\n { label: \"Collections (all)\", path: \"/home/collections\" },\n] as const;\n\nconst RESOURCE_ROUTES = [\n {\n label: \"Category\",\n type: \"category\",\n template: \"/home/categories/%s\",\n fallback: \"/home/categories\",\n },\n {\n label: \"Collection\",\n type: \"collection\",\n template: \"/home/collections/%s\",\n fallback: \"/home/collections\",\n },\n {\n label: \"Product\",\n type: \"product\",\n template: \"/home/products/%s\",\n fallback: \"/home/shop\",\n },\n {\n label: \"Library\",\n type: \"library\",\n template: \"/home/libraries/%s\",\n fallback: \"/home/libraries\",\n },\n {\n label: \"Post\",\n type: \"post\",\n template: \"/home/posts/%s\",\n fallback: \"/home/blog\",\n },\n {\n label: \"Media\",\n type: \"medium\",\n template: \"/home/media/%s\",\n fallback: \"/home/media\",\n },\n {\n label: \"Enrollment Pack\",\n type: \"enrollment_pack\",\n template: \"/home/enrollments/%s\",\n fallback: \"/home/join\",\n },\n {\n label: \"Page\",\n type: \"page\",\n template: \"/home/pages/%s\",\n fallback: \"/home/pages\",\n },\n] as const;\n\nasync function fetchTemplatesForType(\n api: ReturnType<typeof createApiClient>,\n themeId: number,\n themeableType: string,\n): Promise<ThemeTemplate[]> {\n const params = new URLSearchParams({\n application_theme_id: String(themeId),\n themeable_type: themeableType,\n published: \"true\",\n });\n const body = await api.get<TemplatesResponse>(\n `/api/application_theme_templates?${params}`,\n );\n return body.templates ?? [];\n}\n\nasync function selectTemplate(\n api: ReturnType<typeof createApiClient>,\n themeId: number,\n themeableType: string,\n onCancel: () => void,\n): Promise<number | null> {\n const templates = await fetchTemplatesForType(api, themeId, themeableType);\n if (templates.length <= 1) return null;\n\n const templateChoices = templates.map((t) => ({\n title: `${t.name}${t.default ? \" (default)\" : \"\"}`,\n value: t.id,\n }));\n const { templateId } = await prompts(\n {\n type: \"autocomplete\",\n name: \"templateId\",\n message: \"Select a template\",\n choices: templateChoices,\n suggest: (input: string, choices: prompts.Choice[]) =>\n Promise.resolve(localSuggest(input, choices)),\n },\n { onCancel },\n );\n\n return templateId ?? null;\n}\n\nexport function createNavigateCommand(): Command {\n return new Command(\"navigate\")\n .description(\"Interactively navigate to a route in the dev server browser\")\n .option(\"--host <host>\", \"Dev server host\", \"127.0.0.1\")\n .option(\"--port <port>\", \"Dev server port\", \"9292\")\n .option(\"-t, --theme <id>\", \"Theme ID (defaults to active dev theme)\")\n .action(async (opts: { host: string; port: string; theme?: string }) => {\n requireToken();\n\n const themeId = opts.theme ? Number(opts.theme) : getLastDevThemeId();\n\n if (!themeId) {\n console.error(\n \"No active dev theme. Run `fluid theme dev` first, or pass --theme <id>.\",\n );\n process.exit(1);\n }\n\n const address = `http://${opts.host}:${opts.port}`;\n\n type Choice = {\n title: string;\n value:\n | string\n | {\n resourceType: string;\n template: string;\n fallback: string;\n label: string;\n };\n };\n const choices: Choice[] = [\n ...STATIC_ROUTES.map((r) => ({ title: r.label, value: r.path })),\n ...RESOURCE_ROUTES.map((r) => ({\n title: `${r.label} (select specific)`,\n value: {\n resourceType: r.type,\n template: r.template,\n fallback: r.fallback,\n label: r.label,\n },\n })),\n ];\n\n const onCancel = () => process.exit(130);\n\n const { dest } = await prompts(\n {\n type: \"autocomplete\",\n name: \"dest\",\n message: \"Select a route\",\n choices,\n suggest: (input: string, choices: prompts.Choice[]) =>\n Promise.resolve(localSuggest(input, choices)),\n },\n { onCancel },\n );\n\n if (!dest) return;\n\n const api = createApiClient();\n let path: string;\n let themeableType: string | undefined;\n\n if (typeof dest === \"string\") {\n path = dest;\n themeableType = THEMEABLE_TYPE_MAP[dest];\n } else {\n themeableType = dest.resourceType;\n const body = await themes.getApplicationThemeAvailableThemeables(\n api,\n themeId,\n { themeable: dest.resourceType, per_page: 50 },\n );\n const resources = body.available_themeables ?? [];\n\n if (!resources.length) {\n console.log(`No ${dest.label} resources found, using listing page.`);\n path = dest.fallback;\n } else {\n const resourceChoices = resources.map((r) => ({\n title: r.title ?? r.slug ?? \"Untitled\",\n value: r.slug,\n }));\n const { slug } = await prompts(\n {\n type: \"autocomplete\",\n name: \"slug\",\n message: `Select a ${dest.label.toLowerCase()}`,\n choices: resourceChoices,\n suggest: (input: string, choices: prompts.Choice[]) =>\n Promise.resolve(localSuggest(input, choices)),\n },\n { onCancel },\n );\n path = dest.template.replace(\"%s\", slug as string);\n }\n }\n\n let templateParam = \"\";\n if (themeableType) {\n const templateId = await selectTemplate(\n api,\n themeId,\n themeableType,\n onCancel,\n );\n if (templateId) {\n templateParam = `?theme_template_id=${templateId}`;\n }\n }\n\n const url = `${address}${path}${templateParam}`;\n console.log(`\\nNavigating to: ${url}\\n`);\n const open = (await import(\"open\")).default;\n await open(url);\n });\n}\n","import {\n cpSync,\n existsSync,\n mkdirSync,\n readdirSync,\n renameSync,\n rmSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\n\n// A skill is a directory containing a SKILL.md. The bundled skills directory\n// holds one such directory per skill (e.g. `themes-review/`).\nexport function listSkillNames(skillsDir: string): string[] {\n if (!existsSync(skillsDir)) return [];\n return readdirSync(skillsDir, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name)\n .filter((name) => existsSync(join(skillsDir, name, \"SKILL.md\")))\n .sort();\n}\n\nexport interface InstallSkillsOptions {\n /** Directory holding the bundled skills (one sub-directory per skill). */\n sourceDir: string;\n /** Directory the skills are copied into (one sub-directory per skill). */\n targetRoot: string;\n /** Overwrite existing skills without asking. */\n force: boolean;\n /**\n * Asked once per skill that already exists when not forcing. Return true to\n * overwrite, false to leave the existing copy untouched.\n */\n confirmOverwrite: (name: string) => Promise<boolean>;\n /**\n * Called immediately for each install that left a temporary backup directory\n * behind (it could not be removed). Reported as it happens — not via the\n * return value — so the warning isn't lost if a later skill throws mid-loop.\n */\n onLeftover: (path: string) => void;\n}\n\nexport interface InstallSkillsResult {\n readonly installed: readonly string[];\n readonly skipped: readonly string[];\n}\n\n// Copy each bundled skill into `targetRoot/<name>`. Existing skills are only\n// replaced with `force` or an affirmative `confirmOverwrite`; everything else is\n// reported as skipped so the caller can summarize what happened.\nexport async function installSkills(\n options: InstallSkillsOptions,\n): Promise<InstallSkillsResult> {\n const { sourceDir, targetRoot, force, confirmOverwrite, onLeftover } =\n options;\n\n const installed: string[] = [];\n const skipped: string[] = [];\n\n mkdirSync(targetRoot, { recursive: true });\n\n for (const name of listSkillNames(sourceDir)) {\n const from = join(sourceDir, name);\n const to = join(targetRoot, name);\n const exists = existsSync(to);\n\n if (exists && !force && !(await confirmOverwrite(name))) {\n skipped.push(name);\n continue;\n }\n\n // Atomic copy + swap: a failed copy never destroys an existing install, and\n // the whole-directory replace prunes files removed from the bundled skill.\n // Report any leftover backup immediately so the warning isn't lost if a\n // later skill throws mid-loop.\n const leftover = replaceDirectory(from, to);\n if (leftover !== null) onLeftover(leftover);\n installed.push(name);\n }\n\n return { installed, skipped };\n}\n\n/**\n * Replace `target` with a fresh copy of `source` without ever leaving `target`\n * missing or partially written.\n *\n * Filesystem copies are not atomic, so a naive \"delete then copy\" loses the\n * original if the copy fails (permissions, no disk space, an interrupted\n * process). This stages the copy in a sibling directory and only swaps it into\n * place once it has fully succeeded; an existing `target` is moved aside to a\n * sibling backup first and restored if the swap fails. Because the whole\n * directory is replaced, files removed or renamed in `source` do not linger.\n *\n * Staging and backup directories live beside `target`, so its parent must\n * already exist and be on the same filesystem — that keeps the swap a cheap,\n * atomic rename rather than a cross-device copy.\n *\n * Not safe against a second process racing on the same `target`; intended for\n * single-process CLI use.\n *\n * @returns the path of a leftover backup directory that could not be removed\n * after an otherwise-successful replace (the previous contents are retained\n * there for manual cleanup), or `null` when nothing was left behind. The caller\n * should surface a non-null result so the leftover isn't silently hidden.\n */\nexport function replaceDirectory(\n source: string,\n target: string,\n): string | null {\n const staging = reserveSiblingPath(target, \"staging\");\n try {\n cpSync(source, staging, { recursive: true });\n } catch (error) {\n removeQuietly(staging);\n throw error;\n }\n\n // No existing target: a single rename moves the staged copy into place.\n if (!existsSync(target)) {\n return swapIntoPlace(staging, target, null);\n }\n\n // Existing target: move it aside first so the swap stays reversible.\n const backup = reserveSiblingPath(target, \"backup\");\n try {\n renameSync(target, backup);\n } catch (error) {\n removeQuietly(staging);\n throw error;\n }\n return swapIntoPlace(staging, target, backup);\n}\n\n// Move the staged copy into `target`. If the move fails, restore the original\n// from `backup` (when there was one) *before* any cleanup, so a failed staging\n// removal can never leave the caller without a directory at `target`; then\n// best-effort discard the staged copy and rethrow. On success, returns the\n// backup path if it could not be removed (a leftover the caller should report),\n// or `null`.\nfunction swapIntoPlace(\n staging: string,\n target: string,\n backup: string | null,\n): string | null {\n try {\n renameSync(staging, target);\n } catch (error) {\n if (backup !== null) restoreBackup(backup, target, error);\n removeQuietly(staging);\n throw error;\n }\n return removeQuietly(backup);\n}\n\n// Best-effort restore of the original directory. If even this fails, surface an\n// error pointing at the backup so the user can recover their data by hand.\nfunction restoreBackup(backup: string, target: string, cause: unknown): void {\n try {\n renameSync(backup, target);\n } catch {\n throw new Error(\n `Failed to replace ${target}; its previous contents are preserved at ${backup}.`,\n { cause },\n );\n }\n}\n\n// Best-effort removal of a temporary staging/backup directory. Cleanup must\n// never throw: a failure here should not mask the real outcome or strand the\n// caller. Returns the path when removal failed (so the caller can surface the\n// leftover), or `null` when the directory was removed or there was none.\nfunction removeQuietly(path: string | null): string | null {\n if (path === null) return null;\n try {\n rmSync(path, { recursive: true, force: true });\n return null;\n } catch {\n return path;\n }\n}\n\n// Pick a sibling path of `basePath` that does not exist yet, so renaming onto it\n// is a clean create on every platform (Windows rejects a rename onto an existing\n// directory). Deterministic — no randomness.\nfunction reserveSiblingPath(basePath: string, label: string): string {\n let candidate = `${basePath}.${label}`;\n for (let n = 1; existsSync(candidate); n += 1) {\n candidate = `${basePath}.${label}.${n}`;\n }\n return candidate;\n}\n","import { dirname, join, relative, resolve, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport chalk from \"chalk\";\nimport { Command } from \"commander\";\nimport prompts from \"prompts\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { installSkills, listSkillNames } from \"../skills/install.js\";\n\n// Where skills land by default — `.agents/skills/` is the tool-neutral\n// convention for agent skills.\nconst DEFAULT_TARGET_DIR = \".agents/skills\";\n\n// The bundled skills ship beside the compiled CLI at `dist/skills/` and live at\n// the package root `skills/` in source. Resolve relative to this module so the\n// lookup works whether the code runs from the published bundle or from source.\nfunction resolveBundledSkillsDir(): string {\n const here = dirname(fileURLToPath(import.meta.url));\n const candidates = [\n join(here, \"skills\"), // dist/skills (bundled build output)\n join(here, \"..\", \"skills\"), // package root from dist/index.mjs\n join(here, \"..\", \"..\", \"skills\"), // package root from src/commands/\n ];\n for (const dir of candidates) {\n if (listSkillNames(dir).length > 0) return dir;\n }\n // Last resort: walk up looking for a skills/ dir that holds a skill.\n let dir = here;\n for (let depth = 0; depth < 6; depth++) {\n const candidate = join(dir, \"skills\");\n if (listSkillNames(candidate).length > 0) return candidate;\n dir = dirname(dir);\n }\n throw new Error(\n \"Could not locate the bundled theme skills — this is a packaging bug.\",\n );\n}\n\n// Skills stay out of `fluid theme push/pull/dev/lint` only when some path\n// segment is a dot-directory — that is the sole thing ThemeRoot.glob() skips.\n// Returns true when installing into a *visible* dir inside a theme, where the\n// files would be uploaded as theme content instead of kept local.\nfunction skillsWouldShipWithTheme(cwd: string, targetRoot: string): boolean {\n if (!new ThemeRoot(cwd).isValid()) return false; // not a theme dir — irrelevant\n const rel = relative(cwd, targetRoot);\n if (rel.startsWith(\"..\")) return false; // outside the theme root — never scanned\n return !rel.split(sep).some((segment) => segment.startsWith(\".\"));\n}\n\nexport function createSkillsCommand(): Command {\n const skills = new Command(\"skills\").description(\n \"Manage the bundled Fluid theme AI skills\",\n );\n\n skills\n .command(\"install\")\n .description(\n \"Copy the bundled theme skills into the current directory (default: .agents/skills/)\",\n )\n .option(\"-d, --dir <path>\", \"Directory to install into\", DEFAULT_TARGET_DIR)\n .option(\"-f, --force\", \"Overwrite existing skills without prompting\")\n .action(async (opts: { dir: string; force?: boolean }) => {\n const sourceDir = resolveBundledSkillsDir();\n if (listSkillNames(sourceDir).length === 0) {\n console.error(\"No bundled skills found to install.\");\n process.exit(1);\n }\n\n const targetRoot = resolve(process.cwd(), opts.dir);\n if (skillsWouldShipWithTheme(process.cwd(), targetRoot)) {\n console.log(\n `${chalk.yellow(\"⚠\")} ${chalk.bold(opts.dir)} is not a hidden directory — ` +\n `'fluid theme push' will upload these skills as theme files. ` +\n `Install into a dot-directory like ${chalk.cyan(DEFAULT_TARGET_DIR)} to keep them local.`,\n );\n }\n\n const { installed, skipped } = await installSkills({\n sourceDir,\n targetRoot,\n force: Boolean(opts.force),\n confirmOverwrite: async (name) => {\n const res = await prompts(\n {\n type: \"confirm\",\n name: \"overwrite\",\n message: `${chalk.yellow(name)} already exists in ${opts.dir}. Overwrite?`,\n initial: false,\n },\n { onCancel: () => process.exit(130) },\n );\n return Boolean(res.overwrite);\n },\n // Warn as soon as a leftover is found, so it's reported even if a later\n // skill fails before the install finishes.\n onLeftover: (path) => {\n console.log(\n `${chalk.yellow(\"⚠\")} kept the previous copy at ${path} (couldn't remove it — delete it manually)`,\n );\n },\n });\n\n for (const name of installed) {\n console.log(`${chalk.green(\"✓\")} ${name} → ${join(opts.dir, name)}`);\n }\n for (const name of skipped) {\n console.log(`${chalk.dim(`· skipped ${name} (kept existing)`)}`);\n }\n\n const parts = [\n installed.length > 0 ? `${installed.length} installed` : null,\n skipped.length > 0 ? `${skipped.length} skipped` : null,\n ].filter(Boolean);\n console.log(\n `\\n${chalk.bold(parts.join(\", \") || \"Nothing to do\")} in ${targetRoot}`,\n );\n if (installed.length > 0) {\n console.log(\n chalk.dim(\"Restart your agent session to pick up the new skills.\"),\n );\n }\n });\n\n return skills;\n}\n","import { Command } from \"commander\";\nimport type { PluginContext } from \"@fluid-app/fluid-cli\";\nimport { createDevCommand } from \"./dev.js\";\nimport { createPushCommand } from \"./push.js\";\nimport { createPullCommand } from \"./pull.js\";\nimport { createLintCommand } from \"./lint.js\";\nimport { createInitCommand } from \"./init.js\";\nimport { createNavigateCommand } from \"./navigate.js\";\nimport { createSkillsCommand } from \"./skills.js\";\n\nexport function registerThemeCommand(ctx: PluginContext): void {\n const cmd = new Command(\"theme\").description(\n \"Theme developer workflow — dev server, push, pull, lint, init, skills\",\n );\n\n cmd.addCommand(createDevCommand());\n cmd.addCommand(createPushCommand());\n cmd.addCommand(createPullCommand());\n cmd.addCommand(createLintCommand());\n cmd.addCommand(createInitCommand());\n cmd.addCommand(createNavigateCommand());\n cmd.addCommand(createSkillsCommand());\n\n ctx.program.addCommand(cmd);\n}\n","import type { FluidPlugin, PluginContext } from \"@fluid-app/fluid-cli\";\nimport { registerThemeCommand } from \"./commands/theme.js\";\n\nconst plugin: FluidPlugin = {\n name: \"@fluid-app/fluid-cli-theme-dev\",\n version: \"0.1.0\",\n register(ctx: PluginContext) {\n registerThemeCommand(ctx);\n },\n};\n\nexport default plugin;\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["settingTypesJson.types","parseDocument","isRecord","themes.listThemeResources","themes.getThemeAssets","isRecord","themes.createFileResource","themes.destroyFileResource","themes.updateThemeResource","themes.deleteThemeResource","themes.listApplicationThemes","themes.getApplicationTheme","themes.cloneApplicationThemeForDevelopment","themes.createApplicationTheme","themes.getApplicationTheme","themes.createApplicationTheme","themes.getApplicationTheme","themes.publishApplicationTheme","themes.getApplicationThemeAvailableThemeables"],"sources":["../../../platform/api-client-core/src/api-error-shape.ts","../../../platform/api-client-core/src/fetch-client.ts","../src/api.ts","../src/theme-config.ts","../src/plugin-state.ts","../src/theme/mime-type.ts","../src/theme/resource-key.ts","../../../platform/theme-schema/src/setting-types.json","../../../platform/theme-schema/src/types.ts","../../../platform/theme-schema/src/validate-settings.ts","../../../platform/theme-schema/src/validate-blocks.ts","../../../platform/theme-schema/src/validate.ts","../../../platform/theme-schema/src/sections.ts","../src/theme/file.ts","../src/theme/fluid-ignore.ts","../src/theme/root.ts","../src/theme/dev-server/sse.ts","../src/theme/dev-server/hot-reload.ts","../src/theme/dev-server/proxy.ts","../../../api-clients/themes/src/namespaces/v0.ts","../src/theme/format-error.ts","../src/theme/dev-server/watcher.ts","../src/theme/background-pull-guard.ts","../src/theme/case-collisions.ts","../src/theme/asset-manifest.ts","../src/theme/stylesheet-keys.ts","../src/theme/syncer.ts","../src/theme/liquid-delimiters.ts","../src/theme/dev-server/port-preflight.ts","../src/theme/dev-server/index.ts","../src/theme/dev-remote-baseline.ts","../src/theme/shadow-repo.ts","../src/theme-picker.ts","../src/workspace.ts","../src/commands/dev.ts","../src/theme/merge-push.ts","../src/theme/auto-baseline.ts","../src/theme/legacy-migration.ts","../src/theme/sync-identity.ts","../src/commands/push.ts","../src/theme/background-pull.ts","../src/theme/merge-pull.ts","../src/commands/pull.ts","../src/commands/lint.ts","../src/commands/init.ts","../src/commands/navigate.ts","../src/skills/install.ts","../src/commands/skills.ts","../src/commands/theme.ts","../src/index.ts"],"sourcesContent":["/**\n * The shape contract for `ApiError.body` and `ApiError.data`.\n *\n * One decision, in one place: `body` is the response envelope exactly as the\n * server sent it, `data` is the unwrapped field-error bag. Rationale and the\n * alternatives considered live in `docs/api-error-data-contract.md`.\n */\n\n/**\n * The parsed JSON error response body, exactly as the server sent it.\n *\n * This is the envelope: `errors`, `error_message`, `error`, `status`, `meta`\n * and `request_id` all live here. `null` when the response carried no JSON\n * object — HTML, an empty body, a JSON primitive, or unparseable text.\n */\nexport type ApiErrorBody = Record<string, unknown> | null;\n\n/**\n * The field-level error bag: the envelope's `errors` value when the body has\n * one, otherwise the body itself.\n *\n * `errors` is declared `never` because unwrapping already consumed it — read\n * the envelope from `ApiError.body`, not from here. That makes the historical\n * mistake (`error.data.errors.some_field`, which silently resolved to\n * `undefined`) a typecheck failure instead of a degraded error message.\n */\nexport type ApiErrorFieldErrors = Record<string, unknown> & {\n errors?: never;\n};\n\n/**\n * Every value `ApiError.data` can hold. Fluid endpoints return an object bag,\n * but `errors` is occasionally an array or a string, and non-JSON responses\n * carry no data at all.\n */\nexport type ApiErrorData =\n | ApiErrorFieldErrors\n | readonly unknown[]\n | string\n | number\n | boolean\n | null;\n\n/**\n * Narrows a parsed JSON error payload to the envelope contract. Arrays and\n * primitives are not envelopes, so they resolve to `null` — their content is\n * still reachable through `ApiError.data`.\n */\nexport function toApiErrorBody(value: unknown): ApiErrorBody {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return null;\n }\n\n return value as Record<string, unknown>;\n}\n\n/**\n * The single unwrap rule, shared by every producer of `ApiError`.\n *\n * `??` rather than `||` so an explicitly empty `errors` (`\"\"`, `0`, `false`)\n * is preserved instead of silently falling back to the whole envelope.\n */\nexport function toApiErrorData(body: ApiErrorBody): ApiErrorData {\n if (!body) {\n return null;\n }\n\n return (body.errors ?? body) as ApiErrorData;\n}\n","/**\n * Minimal, framework-agnostic fetch client for Fluid APIs\n * Compatible with fluid-admin patterns but usable standalone\n */\n\nimport type { ApiErrorBody, ApiErrorData } from \"./api-error-shape\";\nimport { toApiErrorBody, toApiErrorData } from \"./api-error-shape\";\n\nexport interface FetchClientConfig {\n /**\n * Base URL for all requests (e.g., \"https://api.fluid.app/api\")\n */\n baseUrl: string;\n\n /**\n * Optional function to get auth token\n * Return null/undefined if no token available\n */\n getAuthToken?: () => string | null | Promise<string | null>;\n\n /**\n * Optional callback when 401 auth error occurs\n */\n onAuthError?: () => void;\n\n /**\n * Default headers to include in all requests\n * Example: { \"x-fluid-client\": \"admin\" }\n */\n defaultHeaders?: Record<string, string>;\n\n /**\n * Credentials mode for fetch requests.\n * Set to `\"include\"` for cookie-based (same-origin BFF) authentication.\n * @default undefined (browser default: \"same-origin\")\n */\n credentials?: RequestCredentials;\n\n /**\n * Request cache mode for fetch requests.\n * @default undefined (browser default)\n */\n cache?: RequestCache;\n\n /**\n * Retry configuration for thrown network errors from fetch.\n * Does not retry HTTP error responses or aborted requests.\n * @default undefined (no retries)\n */\n networkRetry?: {\n maxRetries?: number;\n baseDelayMs?: number;\n };\n\n /**\n * Throw ApiError when a successful response declares JSON but cannot be parsed.\n * Defaults to false to preserve the legacy generated-client behavior.\n * @default false\n */\n throwOnInvalidJson?: boolean;\n}\n\nexport interface RequestOptions {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\";\n headers?: Record<string, string>;\n params?: Record<string, unknown>;\n body?: unknown;\n signal?: AbortSignal;\n priority?: RequestInit[\"priority\"];\n}\n\n/**\n * API Error class compatible with fluid-admin's ApiError\n */\nexport class ApiError extends Error {\n public readonly status: number;\n\n /**\n * The unwrapped field-error bag. See `ApiErrorData`; the contract is\n * documented in `docs/api-error-data-contract.md`.\n */\n public readonly data: ApiErrorData;\n\n /**\n * The full error response envelope. Read `error_message`, `error`, `status`\n * and nested `errors` from here — `data` has already unwrapped one level.\n */\n public readonly body: ApiErrorBody;\n\n public readonly requestId?: string;\n\n constructor(\n message: string,\n status: number,\n data?: ApiErrorData,\n requestId?: string,\n body?: ApiErrorBody,\n ) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.data = data ?? null;\n this.body = body ?? null;\n this.requestId = requestId;\n\n if (\"captureStackTrace\" in Error) {\n (\n Error as {\n captureStackTrace: (\n target: Error,\n constructor: NewableFunction,\n ) => void;\n }\n ).captureStackTrace(this, ApiError);\n }\n }\n\n toJSON(): {\n name: string;\n message: string;\n status: number;\n data: ApiErrorData;\n body: ApiErrorBody;\n requestId?: string;\n } {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n data: this.data,\n body: this.body,\n requestId: this.requestId,\n };\n }\n}\n\nfunction getStringRequestId(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction getRequestIdFromHeaders(headers: Headers): string | undefined {\n return (\n getStringRequestId(headers.get(\"x-request-id\")) ??\n getStringRequestId(headers.get(\"request-id\")) ??\n getStringRequestId(headers.get(\"X-Request-ID\"))\n );\n}\n\nfunction getRequestIdFromJsonBody(body: unknown): string | undefined {\n if (!body || typeof body !== \"object\" || Array.isArray(body)) {\n return undefined;\n }\n\n const record = body as Record<string, unknown>;\n const meta = record.meta;\n\n return (\n getStringRequestId(record.request_id) ??\n getStringRequestId(record.requestId) ??\n (meta && typeof meta === \"object\" && !Array.isArray(meta)\n ? (getStringRequestId((meta as Record<string, unknown>).request_id) ??\n getStringRequestId((meta as Record<string, unknown>).requestId))\n : undefined)\n );\n}\n\n/**\n * Type guard for ApiError\n */\nexport function isApiError(error: unknown): error is ApiError {\n return error instanceof ApiError;\n}\n\nexport interface FetchClientInstance {\n request: <TResponse = unknown>(\n endpoint: string,\n options?: RequestOptions,\n ) => Promise<TResponse>;\n requestWithFormData: <TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options?: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n },\n ) => Promise<TResponse>;\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ) => Promise<TResponse>;\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ) => Promise<TResponse>;\n}\n\n/**\n * Creates a configured fetch client instance\n */\nexport function createFetchClient(\n config: FetchClientConfig,\n): FetchClientInstance {\n const {\n baseUrl,\n getAuthToken,\n onAuthError,\n defaultHeaders = {},\n credentials,\n cache,\n networkRetry,\n throwOnInvalidJson = false,\n } = config;\n const maxNetworkRetries = Math.max(0, networkRetry?.maxRetries ?? 0);\n const baseNetworkRetryDelayMs = Math.max(0, networkRetry?.baseDelayMs ?? 0);\n\n /**\n * Build headers for a request\n */\n async function buildHeaders(\n customHeaders?: Record<string, string>,\n ): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...defaultHeaders,\n ...customHeaders,\n };\n\n // Add auth token if available\n if (getAuthToken) {\n const token = await getAuthToken();\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n }\n\n return headers;\n }\n\n /**\n * Join baseUrl + endpoint via string concatenation (matches fetchApi).\n * Using `new URL(endpoint, baseUrl)` would strip any path prefix from\n * baseUrl (e.g. \"/api\") when the endpoint starts with \"/\".\n */\n function joinUrl(endpoint: string): string {\n return `${baseUrl}${endpoint}`;\n }\n\n /**\n * Build URL with query parameters for GET requests\n * Compatible with fluid-admin's query param handling\n */\n function buildUrl(\n endpoint: string,\n params?: Record<string, unknown>,\n ): string {\n const fullUrl = joinUrl(endpoint);\n\n if (!params || Object.keys(params).length === 0) {\n return fullUrl;\n }\n\n const queryString = new URLSearchParams();\n\n Object.entries(params).forEach(([key, value]) => {\n if (value === undefined || value === null) {\n return; // Skip undefined/null values\n }\n\n if (Array.isArray(value)) {\n // Handle arrays like Rails expects: key[]\n value.forEach((item) => queryString.append(`${key}[]`, String(item)));\n } else if (typeof value === \"object\") {\n // Handle nested objects: key[subkey]\n Object.entries(value).forEach(([subKey, subValue]) => {\n if (subValue === undefined || subValue === null) {\n return;\n }\n\n if (Array.isArray(subValue)) {\n subValue.forEach((item) =>\n queryString.append(`${key}[${subKey}][]`, String(item)),\n );\n } else {\n queryString.append(`${key}[${subKey}]`, String(subValue));\n }\n });\n } else {\n queryString.append(key, String(value));\n }\n });\n\n const qs = queryString.toString();\n return qs ? `${fullUrl}?${qs}` : fullUrl;\n }\n\n /**\n * Shared response handler for both JSON and FormData requests.\n * Handles auth errors, non-OK responses, 204 No Content, and JSON parsing.\n */\n async function handleResponse<TResponse>(\n response: Response,\n method: string,\n _url: string,\n ): Promise<TResponse> {\n const headerRequestId = getRequestIdFromHeaders(response.headers);\n\n if (response.status === 401 && onAuthError) {\n onAuthError();\n }\n\n if (!response.ok) {\n // Read body as text first to avoid SyntaxError from response.json()\n // when server returns non-JSON bodies with application/json content-type.\n const errorText = await response.text().catch(() => \"\");\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n // Parsed as `unknown` and narrowed before any property read: a body of\n // `null` (or a bare string/number/array) is valid JSON, and reading\n // `.error` off it directly threw a TypeError instead of an ApiError.\n let parsed: unknown;\n try {\n parsed = JSON.parse(errorText);\n } catch {\n throw new ApiError(\n errorText.slice(0, 200) ||\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n headerRequestId,\n );\n }\n\n const body = toApiErrorBody(parsed);\n // Only an object envelope can carry a message. Some Rails BFF endpoints\n // return `{ error: { message, details } }` instead of `{ message }` or\n // `{ error_message }`, so that shape is tried last.\n const msg = body\n ? (() => {\n const nestedError =\n typeof body.error === \"object\" && body.error !== null\n ? (body.error as { message?: unknown }).message\n : undefined;\n const directError =\n typeof body.error === \"string\" ? body.error : undefined;\n const message =\n typeof body.message === \"string\" ? body.message : undefined;\n const errorMessage =\n typeof body.error_message === \"string\"\n ? body.error_message\n : undefined;\n return (\n message ||\n errorMessage ||\n directError ||\n (typeof nestedError === \"string\" ? nestedError : undefined)\n );\n })()\n : undefined;\n\n throw new ApiError(\n msg || `${method} request failed with status ${response.status}`,\n response.status,\n body ? toApiErrorData(body) : (parsed as ApiErrorData),\n headerRequestId ?? getRequestIdFromJsonBody(parsed),\n body,\n );\n } else {\n throw new ApiError(\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n headerRequestId,\n );\n }\n }\n\n if (\n response.status === 204 ||\n response.headers.get(\"content-length\") === \"0\"\n ) {\n return null as TResponse;\n }\n\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n const responseText = await response.text();\n\n try {\n const data = JSON.parse(responseText);\n return data as TResponse;\n } catch {\n if (throwOnInvalidJson) {\n throw new ApiError(\n \"Failed to parse response as JSON\",\n response.status,\n null,\n headerRequestId,\n );\n }\n\n // API declared JSON content-type but body isn't valid JSON.\n // Return the raw payload to preserve the legacy non-strict path.\n return responseText ? (responseText as TResponse) : (null as TResponse);\n }\n }\n\n // Non-JSON response (text/plain, text/html, etc.)\n return null as TResponse;\n }\n\n function getNetworkRetryDelayMs(retryAttempt: number): number {\n return baseNetworkRetryDelayMs * 2 ** (retryAttempt - 1);\n }\n\n async function waitForNetworkRetry(retryAttempt: number): Promise<void> {\n const delayMs = getNetworkRetryDelayMs(retryAttempt);\n if (delayMs <= 0) {\n return;\n }\n\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n\n async function fetchWithNetworkRetry(\n url: string,\n fetchOptions: RequestInit,\n signal?: AbortSignal,\n ): Promise<Response> {\n let retryCount = 0;\n\n while (true) {\n try {\n return await fetch(url, fetchOptions);\n } catch (networkError) {\n if (signal?.aborted || retryCount >= maxNetworkRetries) {\n throw networkError;\n }\n\n retryCount += 1;\n await waitForNetworkRetry(retryCount);\n\n if (signal?.aborted) {\n throw networkError;\n }\n }\n }\n }\n\n /**\n * Main request function\n */\n async function request<TResponse = unknown>(\n endpoint: string,\n options: RequestOptions = {},\n ): Promise<TResponse> {\n const {\n method = \"GET\",\n headers: customHeaders,\n params,\n body,\n signal,\n priority,\n } = options;\n\n const url = params ? buildUrl(endpoint, params) : joinUrl(endpoint);\n\n const headers = await buildHeaders(customHeaders);\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers };\n if (credentials) fetchOptions.credentials = credentials;\n if (cache) fetchOptions.cache = cache;\n if (priority) fetchOptions.priority = priority;\n const serializedBody =\n body && method !== \"GET\" ? JSON.stringify(body) : null;\n if (serializedBody) fetchOptions.body = serializedBody;\n if (signal) fetchOptions.signal = signal;\n response = await fetchWithNetworkRetry(url, fetchOptions, signal);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n /**\n * Request with FormData (for file uploads)\n */\n async function requestWithFormData<TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n } = {},\n ): Promise<TResponse> {\n const {\n method = \"POST\",\n headers: customHeaders,\n signal,\n priority,\n } = options;\n\n const url = joinUrl(endpoint);\n const headers = await buildHeaders(customHeaders);\n\n // Remove Content-Type to let browser set it with boundary\n delete headers[\"Content-Type\"];\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers, body: formData };\n if (credentials) fetchOptions.credentials = credentials;\n if (cache) fetchOptions.cache = cache;\n if (priority) fetchOptions.priority = priority;\n if (signal) fetchOptions.signal = signal;\n response = await fetchWithNetworkRetry(url, fetchOptions, signal);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n // Return client with convenience methods\n return {\n request: request,\n requestWithFormData: requestWithFormData,\n\n // Convenience methods for common HTTP verbs\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"GET\" as const,\n ...(params && { params }),\n }),\n\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"POST\",\n body,\n }),\n\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PUT\",\n body,\n }),\n\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PATCH\",\n body,\n }),\n\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"DELETE\",\n }),\n };\n}\n\nexport type FetchClient = FetchClientInstance;\n","import {\n createFetchClient,\n type FetchClient,\n} from \"@fluid-app/api-client-core\";\nimport { getAuthToken } from \"@fluid-app/fluid-cli\";\n\nexport type ApiClient = FetchClient;\n\n/** Base URL for all API calls. Set FLUID_API_BASE to route through a BFF. */\nfunction getApiBase(): string {\n return process.env[\"FLUID_API_BASE\"] ?? \"https://api.fluid.app\";\n}\n\n// Only the desktop's unattended worker opts in. Gate auth resolution to pace\n// even concurrent identity/resource requests within a background theme pull.\nlet backgroundGate = Promise.resolve();\nasync function backgroundToken(tokenOverride?: string): Promise<string | null> {\n backgroundGate = backgroundGate.then(\n () => new Promise((resolve) => setTimeout(resolve, 1_000)),\n );\n await backgroundGate;\n return tokenOverride ?? getAuthToken() ?? null;\n}\n\nexport function createApiClient(tokenOverride?: string): ApiClient {\n return createFetchClient({\n baseUrl: getApiBase(),\n getAuthToken: () =>\n process.env[\"FLUID_BACKGROUND_SYNC\"] === \"1\"\n ? backgroundToken(tokenOverride)\n : (tokenOverride ?? getAuthToken() ?? null),\n });\n}\n\nexport function requireToken(): string {\n const token = getAuthToken();\n if (!token) {\n console.error(\"Not logged in. Run `fluid login` first.\");\n process.exit(1);\n }\n return token;\n}\n","import { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nexport interface ThemeConfig {\n themeId: number;\n themeName: string;\n company: string;\n /**\n * Server's `content_version_sha` at the moment of the last pull.\n * Sent back to the server as `base_sha` on push — the server rejects\n * with 409 if it no longer matches, so a push against stale data\n * can't silently clobber. Optional so a `.fluid-theme.json` written\n * by an older CLI (checksums-era) still deserializes; when absent,\n * the CLI falls back to the pre-merge-aware behavior (no server-side\n * check).\n */\n baseSha?: string;\n /** Digest of the managed ImageKit references captured with this baseline. */\n assetManifestSha?: string;\n}\n\n/**\n * Shape of a `.fluid-theme.json` written by the pre-shadow-repo CLI.\n * We read it only to migrate the shadow repo forward on first\n * new-CLI use; we never write this shape back.\n */\nexport interface LegacyThemeConfig extends ThemeConfig {\n lastPulledAt?: string | null;\n checksums?: Record<string, string>;\n}\n\nconst CONFIG_FILE = \".fluid-theme.json\";\n\n/**\n * `company` must be the bare subdomain slug — the dev server builds\n * `<company>.fluid.app` from it, so a full domain here produces an\n * unreachable `<slug>.fluid.app.fluid.app` host. Some external tooling\n * writes the full domain (or a URL); accept those and reduce them to\n * the subdomain.\n */\nfunction normalizeCompany(company: string): string {\n const host = company\n .replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, \"\")\n .replace(/[/?#].*$/, \"\");\n return host.replace(/\\.fluid\\.app$/i, \"\");\n}\n\nfunction configPath(themeRoot: string): string {\n return join(themeRoot, CONFIG_FILE);\n}\n\n/** Read `.fluid-theme.json` from a theme directory, or null if it doesn't exist. */\nexport function readThemeConfig(themeRoot: string): ThemeConfig | null {\n const path = configPath(themeRoot);\n if (!existsSync(path)) return null;\n try {\n const raw = readFileSync(path, \"utf-8\");\n const config = JSON.parse(raw) as ThemeConfig;\n if (typeof config.company === \"string\") {\n config.company = normalizeCompany(config.company);\n }\n return config;\n } catch {\n return null;\n }\n}\n\n/**\n * Read a legacy config (pre-shadow-repo). Used only by the migration\n * path on first new-CLI pull to seed shadow HEAD from files whose\n * local content still matches their stored sha256 checksum.\n */\nexport function readLegacyThemeConfig(\n themeRoot: string,\n): LegacyThemeConfig | null {\n const path = configPath(themeRoot);\n if (!existsSync(path)) return null;\n try {\n const raw = readFileSync(path, \"utf-8\");\n return JSON.parse(raw) as LegacyThemeConfig;\n } catch {\n return null;\n }\n}\n\n/** Write `.fluid-theme.json` to a theme directory. */\nexport function writeThemeConfig(themeRoot: string, config: ThemeConfig): void {\n const path = configPath(themeRoot);\n writeFileSync(path, JSON.stringify(config, null, 2) + \"\\n\", \"utf-8\");\n}\n","import { existsSync } from \"node:fs\";\nimport { readConfig, updateConfig } from \"@fluid-app/fluid-cli\";\n\nexport interface DevThemeRef {\n id: number;\n name: string;\n /** Theme that was pulled when this isolated dev target was created. */\n sourceThemeId?: number;\n}\n\ninterface ThemeDevState {\n /**\n * Dev themes keyed per project, so `theme dev` in one working copy never\n * reuses (and clobbers) another project's sandbox theme. See `devThemeKey`.\n * Entries are pruned once their theme directory no longer exists, so the map\n * can't grow without bound as projects (and one-off/temp dirs) come and go.\n */\n devThemes?: Record<string, DevThemeRef>;\n /** Most recently started dev theme — `navigate`'s default target. */\n lastDevThemeId?: number;\n /**\n * Legacy single global dev theme id. Older CLI versions stored one dev theme\n * here regardless of project. Read once for migration (see `getDevTheme`),\n * then dropped in favour of `devThemes`.\n */\n devThemeId?: number;\n /** Legacy companion to `devThemeId`. */\n devThemeName?: string;\n}\n\nconst PLUGIN_KEY = \"theme-dev\";\n\nfunction getState(): ThemeDevState {\n const config = readConfig();\n return (config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {};\n}\n\n/** Extract the absolute theme root from a `company:themeRoot` key. */\nfunction themeRootFromKey(key: string): string {\n const sep = key.indexOf(\":\");\n return sep === -1 ? key : key.slice(sep + 1);\n}\n\n/**\n * Set `key` to `theme`, dropping any entries whose theme directory no longer\n * exists. Tying an entry's lifetime to its directory keeps the map bounded —\n * abandoned/deleted projects fall out the next time `theme dev` runs anywhere.\n */\nfunction withDevTheme(\n existing: Record<string, DevThemeRef> | undefined,\n key: string,\n theme: DevThemeRef,\n): Record<string, DevThemeRef> {\n const next: Record<string, DevThemeRef> = {};\n for (const [k, v] of Object.entries(existing ?? {})) {\n if (existsSync(themeRootFromKey(k))) next[k] = v;\n }\n next[key] = theme;\n return next;\n}\n\n/**\n * Stable key identifying a dev theme's owning project: the Fluid company\n * (subdomains are globally unique) plus the absolute theme root. Two working\n * copies — or the same copy pulled from two companies — get distinct keys.\n */\nexport function devThemeKey(\n company: string | undefined,\n themeRoot: string,\n): string {\n return `${company ?? \"default\"}:${themeRoot}`;\n}\n\n/**\n * The dev theme stored for a project key, if any. Falls back once to the legacy\n * global `devThemeId` (older CLI versions) and adopts it for this key — clearing\n * the legacy fields so a second project can't adopt the same theme and collide.\n */\nexport function getDevTheme(key: string): DevThemeRef | undefined {\n const state = getState();\n const existing = state.devThemes?.[key];\n if (existing) return existing;\n\n if (state.devThemeId) {\n const migrated: DevThemeRef = {\n id: state.devThemeId,\n name: state.devThemeName ?? `Development #${state.devThemeId}`,\n };\n updateConfig((config) => {\n const current = (config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {};\n const { devThemeId: _id, devThemeName: _name, ...rest } = current;\n return {\n ...config,\n plugins: {\n ...config.plugins,\n [PLUGIN_KEY]: {\n ...rest,\n devThemes: withDevTheme(rest.devThemes, key, migrated),\n lastDevThemeId: migrated.id,\n },\n },\n };\n });\n return migrated;\n }\n\n return undefined;\n}\n\n/** Store (or refresh) the dev theme for a project key and mark it most-recent. */\nexport function setDevTheme(key: string, theme: DevThemeRef): void {\n updateConfig((config) => {\n const current = (config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {};\n return {\n ...config,\n plugins: {\n ...config.plugins,\n [PLUGIN_KEY]: {\n ...current,\n devThemes: withDevTheme(current.devThemes, key, theme),\n lastDevThemeId: theme.id,\n },\n },\n };\n });\n}\n\n/** Forget a project's dev theme (it was deleted remotely or is no longer a dev theme). */\nexport function clearDevTheme(key: string): void {\n updateConfig((config) => {\n const current = (config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {};\n const removed = current.devThemes?.[key];\n if (!removed) return config;\n const { [key]: _removed, ...rest } = current.devThemes ?? {};\n const next: ThemeDevState = { ...current, devThemes: rest };\n // Don't leave `navigate` pointing at a theme we just forgot.\n if (current.lastDevThemeId === removed.id) {\n next.lastDevThemeId = undefined;\n }\n return {\n ...config,\n plugins: { ...config.plugins, [PLUGIN_KEY]: next },\n };\n });\n}\n\n/**\n * Mark a theme as the most recently started dev server (`navigate`'s default)\n * without recording it as a project's dev theme — used for the `--theme`\n * escape hatch, which may target an arbitrary (non-dev) theme.\n */\nexport function setLastDevThemeId(id: number): void {\n updateConfig((config) => {\n const current = (config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {};\n return {\n ...config,\n plugins: {\n ...config.plugins,\n [PLUGIN_KEY]: { ...current, lastDevThemeId: id },\n },\n };\n });\n}\n\n/**\n * The dev theme to target by default in `navigate` — the most recently started\n * dev server. Falls back to the legacy global id for users who haven't yet run\n * the per-project `theme dev`.\n */\nexport function getLastDevThemeId(): number | undefined {\n const state = getState();\n return state.lastDevThemeId ?? state.devThemeId;\n}\n","const TEXT_TYPES: Record<string, string> = {\n \".liquid\": \"text/x-liquid\",\n \".json\": \"application/json\",\n \".css\": \"text/css\",\n \".js\": \"application/javascript\",\n \".html\": \"text/html\",\n \".txt\": \"text/plain\",\n \".md\": \"text/markdown\",\n \".svg\": \"image/svg+xml\",\n};\n\nconst BINARY_TYPES: Record<string, string> = {\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".eot\": \"application/vnd.ms-fontobject\",\n \".otf\": \"font/otf\",\n \".pdf\": \"application/pdf\",\n \".zip\": \"application/zip\",\n \".mp4\": \"video/mp4\",\n \".webm\": \"video/webm\",\n \".mp3\": \"audio/mpeg\",\n \".wav\": \"audio/wav\",\n};\n\nexport interface MimeType {\n name: string;\n isText: boolean;\n}\n\nexport function mimeTypeFor(ext: string): MimeType {\n const text = TEXT_TYPES[ext];\n if (text) return { name: text, isText: true };\n\n const binary = BINARY_TYPES[ext];\n if (binary) return { name: binary, isText: false };\n\n return { name: \"application/octet-stream\", isText: false };\n}\n","const THEME_LEVEL_RESOURCE_KEYS = new Set([\n \"global_styles.css\",\n \"styles.css\",\n \"variables.json\",\n]);\n\nconst COMPOSITE_RESOURCE_FILE_NAMES = new Set([\n \"index.liquid\",\n \"styles.css\",\n \"variables.json\",\n]);\n\nfunction hasSafeSegments(key: string): boolean {\n const segments = key.split(\"/\");\n return segments.every(\n (segment) => segment.length > 0 && segment !== \".\" && segment !== \"..\",\n );\n}\n\nexport function normalizeThemeResourceKey(value: string): string {\n return value.replaceAll(\"\\\\\", \"/\");\n}\n\n/**\n * Whether a relative file path is a resource key accepted by the Fluid themes\n * resource API. Local project files (package manifests, QA evidence, scripts,\n * source baselines, and similar agent artifacts) must never be uploaded.\n */\nexport function isThemeResourceKey(relativePath: string): boolean {\n const key = normalizeThemeResourceKey(relativePath);\n if (!hasSafeSegments(key)) return false;\n if (THEME_LEVEL_RESOURCE_KEYS.has(key)) return true;\n\n const segments = key.split(\"/\");\n const prefix = segments[0];\n const fileName = segments.at(-1);\n if (!prefix || !fileName || segments.length < 2) return false;\n\n if (\n prefix === \"assets\" ||\n prefix === \"config\" ||\n prefix === \"locales\" ||\n prefix === \"layouts\"\n ) {\n return true;\n }\n\n return segments.length >= 3 && COMPOSITE_RESOURCE_FILE_NAMES.has(fileName);\n}\n","","import settingTypesJson from \"./setting-types.json\" with { type: \"json\" };\n\nexport type SettingType =\n | \"text\"\n | \"plaintext\"\n | \"rich_text\"\n | \"richtext\"\n | \"textarea\"\n | \"html\"\n | \"html_textarea\"\n | \"url\"\n | \"range\"\n | \"number\"\n | \"select\"\n | \"radio\"\n | \"checkbox\"\n | \"color\"\n | \"color_background\"\n | \"font\"\n | \"font_picker\"\n | \"image\"\n | \"image_picker\"\n | \"video_picker\"\n | \"media_picker\"\n | \"text_alignment\"\n | \"media_fit\"\n | \"corner_radius\"\n | \"padding\"\n | \"border\"\n | \"gradient_overlay\"\n | \"header\"\n | \"product\"\n | \"products\"\n | \"collection\"\n | \"collections\"\n | \"category\"\n | \"categories\"\n | \"blog\"\n | \"posts\"\n | \"post\"\n | \"enrollment\"\n | \"enrollments\"\n | \"enrollment_pack\"\n | \"forms\"\n | \"media\"\n | \"variant\"\n | \"link_list\"\n | \"product_list\"\n | \"products_list\"\n | \"collection_list\"\n | \"collections_list\"\n | \"category_list\"\n | \"categories_list\"\n | \"posts_list\"\n | \"enrollment_list\"\n | \"enrollments_list\"\n | \"blog_list\"\n | \"blogs_list\"\n | \"post_list\"\n | \"enrollment_packs_list\";\n\n// Runtime list loaded from the canonical JSON — used for validation.\nexport const VALID_SETTING_TYPES: readonly string[] = Object.values(\n settingTypesJson.types as Record<string, string[]>,\n).flat();\n\n// Compile-time drift guard: if a type exists in the SettingType union but\n// not in setting-types.json, this object literal will error on the missing key.\n// When adding types to setting-types.json, also add them to SettingType above.\nconst _settingTypeCheck: Record<SettingType, true> = {\n text: true,\n plaintext: true,\n rich_text: true,\n richtext: true,\n textarea: true,\n html: true,\n html_textarea: true,\n url: true,\n range: true,\n number: true,\n select: true,\n radio: true,\n checkbox: true,\n color: true,\n color_background: true,\n font: true,\n font_picker: true,\n image: true,\n image_picker: true,\n video_picker: true,\n media_picker: true,\n text_alignment: true,\n media_fit: true,\n corner_radius: true,\n padding: true,\n border: true,\n gradient_overlay: true,\n header: true,\n product: true,\n products: true,\n collection: true,\n collections: true,\n category: true,\n categories: true,\n blog: true,\n posts: true,\n post: true,\n enrollment: true,\n enrollments: true,\n enrollment_pack: true,\n forms: true,\n media: true,\n variant: true,\n link_list: true,\n product_list: true,\n products_list: true,\n collection_list: true,\n collections_list: true,\n category_list: true,\n categories_list: true,\n posts_list: true,\n enrollment_list: true,\n enrollments_list: true,\n blog_list: true,\n blogs_list: true,\n post_list: true,\n enrollment_packs_list: true,\n} satisfies Record<SettingType, true>;\nvoid _settingTypeCheck;\n\nexport interface SelectOption {\n label: string;\n value: string;\n}\n\nexport interface SchemaSetting {\n type: SettingType;\n id: string;\n default?: string | number | boolean | null;\n label?: string;\n options?: SelectOption[];\n min?: number;\n max?: number;\n step?: number;\n unit?: string;\n content?: string;\n visible_if?: Record<string, unknown>;\n}\n\nexport interface SchemaBlock {\n type: string;\n name?: string;\n limit?: number;\n settings?: SchemaSetting[];\n blocks?: SchemaBlock[];\n}\n\nexport interface SchemaPreset {\n name?: string;\n category?: string;\n settings?: Record<string, unknown>;\n blocks?: Array<{ type: string; settings?: Record<string, unknown> }>;\n}\n\nexport interface SectionSchema {\n name?: string;\n tag?: string;\n class?: string;\n enabled_on?: { templates?: string[] };\n disabled_on?: { templates?: string[] };\n max_blocks?: number;\n settings?: SchemaSetting[];\n blocks?: SchemaBlock[] | Record<string, unknown>;\n presets?: SchemaPreset[];\n}\n\nexport type BlocksSchemaType = \"array\" | \"object\" | \"unknown\";\n\n/**\n * Structured locator describing the schema element a diagnostic refers to.\n *\n * The rule logic (this package) is intentionally position-agnostic so it can\n * be shared by the CLI (`fluid theme push`) and the CodeMirror-based theme /\n * visual editor alike. Position-aware consumers use `target` to map a\n * diagnostic back to a source range without re-deriving the validation rules.\n */\nexport type SettingDiagnosticTarget = {\n kind: \"setting\";\n /** Index of the setting within its `settings` array. */\n index: number;\n /** The setting's `id`, when present — used to locate the offending entry. */\n settingId?: string;\n /** The setting's `type`, when present — used to locate the offending entry. */\n settingType?: string;\n /** Which field the diagnostic concerns. */\n field: \"id\" | \"type\";\n};\n\nexport type BlockDiagnosticTarget = {\n kind: \"block\";\n /** Index of the block within its `blocks` array. */\n index: number;\n /** The block's `type`, when present — used to locate the offending entry. */\n blockType?: string;\n /** Which field the diagnostic concerns. */\n field: \"type\" | \"name\" | \"settings\";\n};\n\nexport type SectionDiagnosticTarget = {\n kind: \"section\";\n /** The referenced section `type` that has no matching section file. */\n sectionType: string;\n /** The `{% section %}` tag's instance id, when the tag included one. */\n tagId?: string;\n};\n\nexport type DiagnosticTarget =\n | SettingDiagnosticTarget\n | BlockDiagnosticTarget\n | SectionDiagnosticTarget;\n\nexport interface Diagnostic {\n severity: \"error\" | \"warning\";\n message: string;\n /**\n * Optional structured locator so position-aware consumers (e.g. the\n * CodeMirror-based theme editor) can map a diagnostic back to a source\n * range. The CLI ignores this and only renders `message`.\n */\n target?: DiagnosticTarget;\n}\n","import { VALID_SETTING_TYPES } from \"./types\";\nimport type { Diagnostic } from \"./types\";\n\n/**\n * Message shown when a setting declares a `type` that is not one of the\n * canonical `VALID_SETTING_TYPES`. Centralized here so the CLI and the editor\n * render identical text. Kept to a single line — the list of valid types is a\n * static set exposed once via the `VALID_SETTING_TYPES` export, so repeating it\n * in every diagnostic only bloats structured output.\n */\nexport function invalidSettingTypeMessage(type: string): string {\n return `Invalid settings type: '${type}'`;\n}\n\nexport function validateSettings(settings: unknown[]): Diagnostic[] {\n const diagnostics: Diagnostic[] = [];\n const ids = new Set<string>();\n\n for (let index = 0; index < settings.length; index++) {\n const raw = settings[index];\n const setting: Record<string, unknown> =\n raw !== null && typeof raw === \"object\" && !Array.isArray(raw)\n ? (raw as Record<string, unknown>)\n : {};\n\n const id = typeof setting.id === \"string\" ? setting.id : undefined;\n const type = typeof setting.type === \"string\" ? setting.type : undefined;\n\n if (id !== undefined && id.trim() === \"\") {\n diagnostics.push({\n severity: \"error\",\n message: \"Error in settings: id cannot be empty\",\n target: { kind: \"setting\", index, settingType: type, field: \"id\" },\n });\n } else if (id && ids.has(id)) {\n diagnostics.push({\n severity: \"error\",\n message: `Error in settings: duplicate id '${id}' found`,\n target: { kind: \"setting\", index, settingId: id, field: \"id\" },\n });\n } else if (id) {\n ids.add(id);\n }\n\n if (!type) {\n diagnostics.push({\n severity: \"error\",\n message: `Error in setting '${id ?? index}': missing required field 'type'`,\n target: { kind: \"setting\", index, settingId: id, field: \"type\" },\n });\n } else if (!VALID_SETTING_TYPES.includes(type)) {\n diagnostics.push({\n severity: \"error\",\n message: invalidSettingTypeMessage(type),\n target: { kind: \"setting\", index, settingType: type, field: \"type\" },\n });\n }\n }\n\n return diagnostics;\n}\n","import type { Diagnostic } from \"./types\";\nimport { validateSettings } from \"./validate-settings\";\n\nexport function validateBlocks(blocks: unknown[]): Diagnostic[] {\n const diagnostics: Diagnostic[] = [];\n const types = new Set<string>();\n\n for (let index = 0; index < blocks.length; index++) {\n const raw = blocks[index];\n const block: Record<string, unknown> =\n raw !== null && typeof raw === \"object\" && !Array.isArray(raw)\n ? (raw as Record<string, unknown>)\n : {};\n\n const type = typeof block.type === \"string\" ? block.type : undefined;\n const name = typeof block.name === \"string\" ? block.name : undefined;\n const settings = block.settings;\n\n if (!type) {\n diagnostics.push({\n severity: \"error\",\n message: `Error in blocks at index ${index}: missing required field 'type'`,\n target: { kind: \"block\", index, field: \"type\" },\n });\n } else if (types.has(type)) {\n diagnostics.push({\n severity: \"warning\",\n message: `Warning in blocks: duplicate type '${type}' found`,\n target: { kind: \"block\", index, blockType: type, field: \"type\" },\n });\n } else {\n types.add(type);\n }\n\n // Named block references (type only, no name or settings) point to\n // standalone block templates — skip the name requirement for those.\n const isNamedBlockRef = !name && !settings;\n if (!name && type !== \"@app\" && type !== \"@theme\" && !isNamedBlockRef) {\n diagnostics.push({\n severity: \"error\",\n message: `Error in block '${type ?? index}': missing required field 'name'`,\n target: { kind: \"block\", index, blockType: type, field: \"name\" },\n });\n }\n\n if (settings) {\n if (!Array.isArray(settings)) {\n // e.g. the author wrote `\"settings\": {}` instead of `\"settings\": []`.\n diagnostics.push({\n severity: \"error\",\n message: `Error in block '${type ?? index}': 'settings' must be an array ([])`,\n target: { kind: \"block\", index, blockType: type, field: \"settings\" },\n });\n } else {\n diagnostics.push(...validateSettings(settings));\n }\n }\n\n // Recurse into nested blocks (max 2 levels enforced by the engine,\n // but we validate whatever is declared)\n if (Array.isArray(block.blocks)) {\n diagnostics.push(...validateBlocks(block.blocks as unknown[]));\n }\n }\n\n return diagnostics;\n}\n","import type { BlocksSchemaType, Diagnostic } from \"./types\";\nimport { validateSettings } from \"./validate-settings\";\nimport { validateBlocks } from \"./validate-blocks\";\n\n// Strip Liquid comment blocks so they don't interfere with schema extraction.\nfunction stripLiquidComments(text: string): string {\n return text.replace(\n /\\{%-?\\s*comment\\s*-?%\\}[\\s\\S]*?\\{%-?\\s*endcomment\\s*-?%\\}/g,\n \"\",\n );\n}\n\n// Detect duplicate \"blocks\" keys in the same JSON object.\n// Standard JSON.parse silently drops duplicates, so we scan tokens manually.\nfunction findDuplicateBlocksKeys(jsonText: string): number {\n let count = 0;\n const stack: Array<{\n type: \"object\" | \"array\";\n keys: Set<string>;\n expectingKey: boolean;\n }> = [];\n let pendingKey: string | null = null;\n let i = 0;\n\n while (i < jsonText.length) {\n const ch = jsonText.charCodeAt(i);\n\n // Whitespace\n if (ch === 0x20 || ch === 0x0a || ch === 0x0d || ch === 0x09) {\n i++;\n continue;\n }\n\n if (ch === 0x7b) {\n // {\n stack.push({ type: \"object\", keys: new Set(), expectingKey: true });\n pendingKey = null;\n i++;\n } else if (ch === 0x7d) {\n // }\n stack.pop();\n pendingKey = null;\n i++;\n } else if (ch === 0x5b) {\n // [\n pendingKey = null;\n stack.push({ type: \"array\", keys: new Set(), expectingKey: false });\n i++;\n } else if (ch === 0x5d) {\n // ]\n stack.pop();\n pendingKey = null;\n i++;\n } else if (ch === 0x3a) {\n // :\n i++;\n } else if (ch === 0x2c) {\n // ,\n const top = stack[stack.length - 1];\n if (top?.type === \"object\") {\n top.expectingKey = true;\n }\n pendingKey = null;\n i++;\n } else if (ch === 0x22) {\n // \"\n let j = i + 1;\n while (j < jsonText.length) {\n if (\n jsonText.charCodeAt(j) === 0x22 &&\n jsonText.charCodeAt(j - 1) !== 0x5c\n ) {\n break;\n }\n j++;\n }\n const str = jsonText.slice(i + 1, j);\n i = j + 1;\n\n const top = stack[stack.length - 1];\n if (top?.type === \"object\" && top.expectingKey) {\n if (str === \"blocks\" && top.keys.has(str)) {\n count++;\n }\n top.keys.add(str);\n top.expectingKey = false;\n pendingKey = str;\n } else {\n pendingKey = null;\n }\n } else {\n pendingKey = null;\n i++;\n }\n }\n\n return count;\n}\n\nexport interface ValidateSchemaOptions {\n blocksSchemaType?: BlocksSchemaType;\n}\n\n// Validate the full Liquid file content containing a {% schema %} block.\n// Returns an array of diagnostics (empty = valid).\nexport function validateSchemaText(\n text: string,\n options?: ValidateSchemaOptions,\n): Diagnostic[] {\n const blocksSchemaType = options?.blocksSchemaType ?? \"unknown\";\n const diagnostics: Diagnostic[] = [];\n\n const stripped = stripLiquidComments(text);\n const match = stripped.match(\n /\\{%-?\\s*schema\\s*-?%\\}([\\s\\S]*?)\\{%-?\\s*endschema\\s*-?%\\}/,\n );\n if (!match) return diagnostics;\n\n const jsonText = match[1] ?? \"\";\n\n let schema: Record<string, unknown>;\n try {\n schema = JSON.parse(jsonText) as Record<string, unknown>;\n } catch (e) {\n diagnostics.push({\n severity: \"error\",\n message: `Invalid JSON:\\n ${(e as Error).message}`,\n });\n return diagnostics;\n }\n\n // Duplicate \"blocks\" keys\n const dupes = findDuplicateBlocksKeys(jsonText);\n for (let d = 0; d < dupes; d++) {\n diagnostics.push({\n severity: \"error\",\n message: \"Error in blocks: duplicate 'blocks' key in the same object\",\n });\n }\n\n // Settings\n if (\n schema !== null &&\n typeof schema === \"object\" &&\n Array.isArray(schema.settings)\n ) {\n diagnostics.push(...validateSettings(schema.settings));\n }\n\n // Blocks\n if (schema !== null && typeof schema === \"object\" && \"blocks\" in schema) {\n const blocks = schema.blocks;\n\n if (blocksSchemaType === \"array\") {\n if (!Array.isArray(blocks)) {\n diagnostics.push({\n severity: \"error\",\n message: \"Error in blocks: expected an array ([])\",\n });\n } else {\n diagnostics.push(...validateBlocks(blocks));\n }\n } else if (blocksSchemaType === \"object\") {\n if (\n Array.isArray(blocks) ||\n typeof blocks !== \"object\" ||\n blocks === null\n ) {\n diagnostics.push({\n severity: \"error\",\n message: \"Error in blocks: expected an object ({})\",\n });\n }\n } else {\n // \"unknown\" — validate if array, accept if object\n if (Array.isArray(blocks)) {\n diagnostics.push(...validateBlocks(blocks));\n }\n }\n }\n\n return diagnostics;\n}\n\n// Validate a parsed schema object directly (when you already have the JSON).\nexport function validateSchema(\n schema: Record<string, unknown>,\n options?: ValidateSchemaOptions,\n): Diagnostic[] {\n const blocksSchemaType = options?.blocksSchemaType ?? \"unknown\";\n const diagnostics: Diagnostic[] = [];\n\n if (Array.isArray(schema.settings)) {\n diagnostics.push(...validateSettings(schema.settings));\n }\n\n if (\"blocks\" in schema) {\n const blocks = schema.blocks;\n\n if (blocksSchemaType === \"array\") {\n if (!Array.isArray(blocks)) {\n diagnostics.push({\n severity: \"error\",\n message: \"Error in blocks: expected an array ([])\",\n });\n } else {\n diagnostics.push(...validateBlocks(blocks));\n }\n } else if (blocksSchemaType === \"object\") {\n if (\n Array.isArray(blocks) ||\n typeof blocks !== \"object\" ||\n blocks === null\n ) {\n diagnostics.push({\n severity: \"error\",\n message: \"Error in blocks: expected an object ({})\",\n });\n }\n } else {\n if (Array.isArray(blocks)) {\n diagnostics.push(...validateBlocks(blocks));\n }\n }\n }\n\n return diagnostics;\n}\n","import type { Diagnostic } from \"./types\";\n\n// Liquid comment blocks — stripped so commented-out section tags are not\n// treated as real references.\nconst LIQUID_COMMENT_REGEX =\n /\\{%-?\\s*comment\\s*-?%\\}[\\s\\S]*?\\{%-?\\s*endcomment\\s*-?%\\}/g;\n\n// The `{% schema %} … {% endschema %}` block — removed so section types\n// declared inside the schema JSON are not mistaken for `{% section %}` usages.\nconst SCHEMA_BLOCK_REGEX =\n /\\{%-?\\s*schema\\s*-?%\\}[\\s\\S]*?\\{%-?\\s*endschema\\s*-?%\\}/;\n\n// Matches a `{% section %}` tag. Supports both the id-bearing form\n// (`{% section 'hero', id: 'abc' %}`) emitted by the visual editor and the\n// bare form (`{% section 'hero' %}`) hand-authored themes use, plus\n// whitespace-control tags (`{%- … -%}`) and single or double quotes.\n// `id` is optional — capture group 2 is undefined for bare tags.\nconst SECTION_TAG_PATTERN =\n \"\\\\{%-?\\\\s*section\\\\s+['\\\"]([^'\\\"]+)['\\\"](?:\\\\s*,\\\\s*id:\\\\s*['\\\"]([^'\\\"]+)['\\\"])?\\\\s*-?%\\\\}\";\n\n// Reserved layout-region section types. `{% section 'navbar' %}` and friends\n// resolve to the theme's navbar/footer/library_navbar template slots rather than\n// a `sections/<name>` definition, and render empty when absent — so they are\n// never \"missing\". Mirrors `LiquidTags::Section::SECTION_TEMPLATES` server-side.\nconst RESERVED_SECTION_TYPES = new Set([\"navbar\", \"library_navbar\", \"footer\"]);\n\n// `fluid://extensions/{id}/{type}/{name}` references resolve against an app\n// extension installed on the company at render time — they cannot be validated\n// against local files, so they are never flagged. Mirrors `Themes::ExtensionUri`.\nconst EXTENSION_URI_PATTERN = /^fluid:\\/\\/extensions\\/[^/]+\\/[^/]+\\/[^/]+$/;\n\n/** Whether a `{% section %}` type resolves to something other than an on-disk `sections/<name>` definition (and so cannot be flagged as missing). */\nexport function isNonLocalSectionType(type: string): boolean {\n return RESERVED_SECTION_TYPES.has(type) || EXTENSION_URI_PATTERN.test(type);\n}\n\nexport interface SectionReference {\n /** The referenced section type/name (group 1). */\n type: string;\n /** The section instance id, when the tag declares one. */\n id?: string;\n /** The full matched tag text. */\n fullTag: string;\n /** 0-based position of the tag within the template body. */\n order: number;\n}\n\n/** A template, identified by `path`, with its raw liquid `content`. */\nexport interface TemplateInput {\n path: string;\n content: string;\n}\n\n// Liquid outside the comment and schema blocks — the rendered template body\n// where `{% section %}` references actually live.\nfunction templateBody(liquid: string): string {\n return liquid\n .replace(LIQUID_COMMENT_REGEX, \"\")\n .replace(SCHEMA_BLOCK_REGEX, \"\");\n}\n\n/**\n * Extract every `{% section %}` reference from a liquid template, ignoring\n * tags inside comments or the `{% schema %}` block. Shared by the editor's\n * section-usage detection and the CLI linter so both parse references\n * identically.\n */\nexport function extractSectionReferences(liquid: string): SectionReference[] {\n const body = templateBody(liquid);\n const pattern = new RegExp(SECTION_TAG_PATTERN, \"g\");\n const references: SectionReference[] = [];\n let order = 0;\n let match: RegExpExecArray | null;\n while ((match = pattern.exec(body)) !== null) {\n const type = match[1];\n if (!type) continue;\n references.push({ type, id: match[2], fullTag: match[0], order: order++ });\n }\n return references;\n}\n\n/**\n * Paths of the templates that reference `sectionName`. Used by the editor to\n * warn before deleting a section that is still in use.\n */\nexport function findTemplatesReferencingSection(\n templates: TemplateInput[],\n sectionName: string,\n): string[] {\n const matches: string[] = [];\n for (const template of templates) {\n const references = extractSectionReferences(template.content);\n if (references.some((reference) => reference.type === sectionName)) {\n matches.push(template.path);\n }\n }\n return matches;\n}\n\nexport interface MissingSectionRef {\n templatePath: string;\n sectionType: string;\n diagnostic: Diagnostic;\n}\n\n/**\n * Find `{% section %}` references that point to a section that does not exist\n * in `existingSectionNames` — the static equivalent of \"an in-use section was\n * deleted\". Reserved layout-region types (navbar/footer/library_navbar) and\n * `fluid://` extension URIs are never flagged (see `isNonLocalSectionType`).\n * Emits one `error` diagnostic per missing section type per template.\n */\nexport function findMissingSectionReferences(\n templates: TemplateInput[],\n existingSectionNames: Set<string>,\n): MissingSectionRef[] {\n const missing: MissingSectionRef[] = [];\n for (const template of templates) {\n const reported = new Set<string>();\n for (const reference of extractSectionReferences(template.content)) {\n if (existingSectionNames.has(reference.type)) continue;\n if (isNonLocalSectionType(reference.type)) continue;\n if (reported.has(reference.type)) continue;\n reported.add(reference.type);\n missing.push({\n templatePath: template.path,\n sectionType: reference.type,\n diagnostic: {\n severity: \"error\",\n message: `references missing section '${reference.type}'`,\n target: {\n kind: \"section\",\n sectionType: reference.type,\n tagId: reference.id,\n },\n },\n });\n }\n }\n return missing;\n}\n","import {\n readFileSync,\n writeFileSync,\n mkdirSync,\n existsSync,\n statSync,\n} from \"node:fs\";\nimport { extname, basename, relative, dirname } from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { mimeTypeFor, type MimeType } from \"./mime-type.js\";\nimport { normalizeThemeResourceKey } from \"./resource-key.js\";\nimport {\n validateSchemaText,\n type Diagnostic,\n type BlocksSchemaType,\n} from \"@fluid-app/theme-schema\";\n\n// Top-level theme folders that are not page templates. Everything else at the\n// top level (home_page, product, page, footer, navbar, …) is a page template.\nconst NON_TEMPLATE_DIRS = new Set([\n \"sections\",\n \"blocks\",\n \"components\",\n \"layouts\",\n \"config\",\n \"assets\",\n \"locales\",\n]);\n\nexport class ThemeFile {\n readonly absolutePath: string;\n readonly relativePath: string;\n readonly mime: MimeType;\n\n constructor(absolutePath: string, root: string) {\n this.absolutePath = absolutePath;\n this.relativePath = normalizeThemeResourceKey(relative(root, absolutePath));\n this.mime = mimeTypeFor(extname(absolutePath).toLowerCase());\n }\n\n get name(): string {\n return basename(this.absolutePath);\n }\n\n get isText(): boolean {\n return this.mime.isText;\n }\n\n get isLiquid(): boolean {\n return this.absolutePath.endsWith(\".liquid\");\n }\n\n get isJson(): boolean {\n return this.absolutePath.endsWith(\".json\");\n }\n\n get exists(): boolean {\n return existsSync(this.absolutePath);\n }\n\n read(): string {\n return readFileSync(this.absolutePath, \"utf-8\");\n }\n\n readBinary(): Buffer {\n return readFileSync(this.absolutePath);\n }\n\n write(content: string | Buffer): void {\n mkdirSync(dirname(this.absolutePath), { recursive: true });\n if (typeof content === \"string\") {\n writeFileSync(this.absolutePath, content, \"utf-8\");\n } else {\n writeFileSync(this.absolutePath, content);\n }\n }\n\n checksum(): string {\n const content = this.isText ? this.read() : this.readBinary();\n return createHash(\"sha256\").update(content).digest(\"hex\");\n }\n\n size(): number {\n return statSync(this.absolutePath).size;\n }\n\n get isTemplate(): boolean {\n // Page templates (home_page, product, footer, navbar, …) live in top-level\n // page-type folders and expect blocks as objects. The reserved categories\n // below either expect blocks as arrays (sections, blocks, components) or\n // carry no block schema (layouts, config, assets, locales).\n const parts = this.relativePath.split(/[/\\\\]/);\n return parts.length >= 2 && !NON_TEMPLATE_DIRS.has(parts[0]!);\n }\n\n validateSchema(): Diagnostic[] {\n if (!this.isLiquid) return [];\n\n const blocksSchemaType: BlocksSchemaType = this.isTemplate\n ? \"object\"\n : \"array\";\n\n return validateSchemaText(this.read(), { blocksSchemaType });\n }\n}\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\n\nconst IGNORE_FILE = \".fluidignore\";\n\ninterface Pattern {\n negated: boolean;\n pattern: string;\n}\n\nexport class FluidIgnore {\n private patterns: Pattern[];\n\n constructor(root: string) {\n this.patterns = this.parse(join(root, IGNORE_FILE));\n }\n\n ignore(relativePath: string): boolean {\n let result = false;\n for (const { negated, pattern } of this.patterns) {\n if (this.match(pattern, relativePath)) {\n result = !negated;\n }\n }\n return result;\n }\n\n private parse(filePath: string): Pattern[] {\n if (!existsSync(filePath)) return [];\n return readFileSync(filePath, \"utf-8\")\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l && !l.startsWith(\"#\"))\n .map((l) => {\n const negated = l.startsWith(\"!\");\n let pattern = negated ? l.slice(1) : l;\n if (pattern.startsWith(\"/\")) pattern = pattern.slice(1);\n return { negated, pattern };\n });\n }\n\n private match(pattern: string, path: string): boolean {\n if (pattern.endsWith(\"/\")) {\n return path.startsWith(pattern) || path === pattern.slice(0, -1);\n }\n if (pattern.includes(\"/\")) {\n return this.fnmatch(pattern, path);\n }\n return this.fnmatch(pattern, path) || this.fnmatch(pattern, basename(path));\n }\n\n private fnmatch(pattern: string, str: string): boolean {\n const re = pattern\n .split(\"**\")\n .map((p) =>\n p\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/\\?/g, \"[^/]\"),\n )\n .join(\".*\");\n return new RegExp(`^${re}$`).test(str);\n }\n}\n","import { existsSync, readdirSync, statSync } from \"node:fs\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport { ThemeFile } from \"./file.js\";\nimport { FluidIgnore } from \"./fluid-ignore.js\";\nimport { isThemeResourceKey } from \"./resource-key.js\";\n\nconst THEME_MARKERS = [\"templates\", \"assets\", \"config\"];\nconst THEME_ASSET_MANIFEST = \".fluid-assets.json\";\n\nexport class ThemeRoot {\n readonly root: string;\n readonly ignore: FluidIgnore;\n\n constructor(root: string) {\n this.root = resolve(root);\n this.ignore = new FluidIgnore(this.root);\n }\n\n isValid(): boolean {\n return (\n existsSync(join(this.root, THEME_ASSET_MANIFEST)) ||\n THEME_MARKERS.some((m) => {\n try {\n return statSync(join(this.root, m)).isDirectory();\n } catch {\n return false;\n }\n })\n );\n }\n\n files(): ThemeFile[] {\n return this.glob(this.root).filter(\n (f) =>\n isThemeResourceKey(f.relativePath) &&\n !this.ignore.ignore(f.relativePath),\n );\n }\n\n isResourcePath(pathOrFile: string | ThemeFile): boolean {\n const file = this.file(pathOrFile);\n return isThemeResourceKey(file.relativePath);\n }\n\n file(pathOrFile: string | ThemeFile): ThemeFile {\n if (pathOrFile instanceof ThemeFile) return pathOrFile;\n const abs = isAbsolute(pathOrFile)\n ? pathOrFile\n : join(this.root, pathOrFile);\n return new ThemeFile(abs, this.root);\n }\n\n private glob(dir: string): ThemeFile[] {\n const results: ThemeFile[] = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name.startsWith(\".\")) continue;\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n if (entry.name === \"node_modules\") continue;\n results.push(...this.glob(full));\n } else if (entry.isFile()) {\n results.push(new ThemeFile(full, this.root));\n }\n }\n return results;\n }\n}\n","import type { ServerResponse } from \"node:http\";\n\nexport class SSEStream {\n private responses = new Set<ServerResponse>();\n\n add(res: ServerResponse): void {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n \"Access-Control-Allow-Origin\": \"*\",\n });\n res.write(\":\\n\\n\");\n this.responses.add(res);\n res.on(\"close\", () => this.responses.delete(res));\n }\n\n broadcast(data: string): void {\n const payload = `data: ${data}\\n\\n`;\n for (const res of this.responses) {\n try {\n res.write(payload);\n } catch {\n this.responses.delete(res);\n }\n }\n }\n\n close(): void {\n for (const res of this.responses) {\n try {\n res.end();\n } catch {\n // ignore\n }\n }\n this.responses.clear();\n }\n\n get size(): number {\n return this.responses.size;\n }\n}\n","export function buildHotReloadScript(mode: \"full-page\" | \"off\"): string {\n return `\n<script>\n(() => {\n window.__FLUID_CLI_ENV__ = ${JSON.stringify({ mode })};\n\n class HotReload {\n static reloadMode() { return window.__FLUID_CLI_ENV__.mode; }\n static isActive() { return HotReload.reloadMode() !== \"off\"; }\n static setHotReloadCookie(files) {\n const expires = new Date(Date.now() + 3000).toUTCString();\n document.cookie = \\`hot_reload_files=\\${files.join(\",\")};expires=\\${expires};path=/\\`;\n }\n static refresh(files) {\n HotReload.setHotReloadCookie(files);\n console.log(\"[HotReload] Refreshing page\");\n window.location.reload();\n }\n }\n\n class SSEClient {\n constructor(url, handler) {\n if (typeof EventSource === \"undefined\") {\n console.error(\"[HotReload] EventSource not supported in this browser.\");\n return;\n }\n console.log(\"[HotReload] Initializing…\");\n this.url = url;\n this.handler = handler;\n }\n connect() {\n const es = new EventSource(this.url);\n es.onopen = () => console.log(\"[HotReload] SSE connected.\");\n es.onerror = () => {\n console.log(\"[HotReload] SSE closed. Reconnecting in 5s…\");\n es.close();\n setTimeout(() => this.connect(), 5000);\n };\n es.onmessage = (msg) => {\n const data = JSON.parse(msg.data);\n if (data.reload_page) { HotReload.refresh([]); return; }\n this.handler(data);\n };\n }\n }\n\n if (HotReload.isActive()) {\n new SSEClient(\"/hot-reload\", (data) => {\n if (data.modified) HotReload.refresh(data.modified);\n }).connect();\n }\n})();\n</script>`;\n}\n\nexport function injectHotReload(\n html: string,\n mode: \"full-page\" | \"off\",\n): string {\n const script = buildHotReloadScript(mode);\n if (html.includes(\"</body>\")) {\n return html.replace(\"</body>\", `${script}\\n</body>`);\n }\n return html + script;\n}\n","import https from \"node:https\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { injectHotReload } from \"./hot-reload.js\";\nimport { getAuthToken } from \"@fluid-app/fluid-cli\";\n\nconst HOP_BY_HOP = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n \"content-security-policy\",\n]);\n\nexport interface ProxyOptions {\n company: string;\n themeId: number;\n reloadMode: \"full-page\" | \"off\";\n pendingFiles?: () => Array<{ relativePath: string; read: () => string }>;\n}\n\nexport async function proxyRequest(\n req: IncomingMessage,\n res: ServerResponse,\n opts: ProxyOptions,\n): Promise<void> {\n const companyHost = `${opts.company}.fluid.app`;\n\n const headers: Record<string, string> = {};\n for (const [k, v] of Object.entries(req.headers)) {\n if (!HOP_BY_HOP.has(k.toLowerCase()) && typeof v === \"string\") {\n headers[k] = v;\n }\n }\n headers[\"host\"] = companyHost;\n headers[\"x-fluid-theme\"] = String(opts.themeId);\n headers[\"user-agent\"] = \"Fluid CLI\";\n headers[\"accept-encoding\"] = \"identity\";\n\n const url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n url.searchParams.set(\"_fd\", \"0\");\n url.searchParams.set(\"pb\", \"0\");\n\n const pending = opts.pendingFiles?.() ?? [];\n const isGet = req.method === \"GET\" || req.method === \"HEAD\";\n let method = req.method ?? \"GET\";\n let body: string | Buffer | undefined;\n\n if (pending.length > 0 && isGet) {\n method = \"POST\";\n const params = new URLSearchParams();\n params.set(\"_method\", req.method ?? \"GET\");\n for (const f of pending) {\n params.set(`replace_templates[${f.relativePath}]`, f.read());\n }\n const token = getAuthToken();\n if (token) headers[\"authorization\"] = `Bearer ${token}`;\n headers[\"content-type\"] = \"application/x-www-form-urlencoded\";\n body = params.toString();\n headers[\"content-length\"] = String(Buffer.byteLength(body));\n } else if (!isGet) {\n body = await readBody(req);\n if (body.length > 0) {\n headers[\"content-length\"] = String(body.length);\n }\n }\n\n return new Promise((resolve, reject) => {\n const options: https.RequestOptions = {\n hostname: companyHost,\n port: 443,\n path: url.pathname + (url.search || \"\"),\n method,\n headers,\n };\n\n const proxyReq = https.request(options, (proxyRes) => {\n const contentType = proxyRes.headers[\"content-type\"] ?? \"\";\n const isHtml = contentType.includes(\"text/html\");\n\n const responseHeaders: Record<string, string | string[]> = {};\n for (const [k, v] of Object.entries(proxyRes.headers)) {\n if (!HOP_BY_HOP.has(k.toLowerCase()) && v !== undefined) {\n responseHeaders[k] = v as string | string[];\n }\n }\n\n if (isHtml) {\n const chunks: Buffer[] = [];\n proxyRes.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n proxyRes.on(\"end\", () => {\n let html = Buffer.concat(chunks).toString(\"utf-8\");\n html = injectHotReload(html, opts.reloadMode);\n responseHeaders[\"content-length\"] = String(Buffer.byteLength(html));\n res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);\n res.end(html);\n resolve();\n });\n } else {\n res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);\n proxyRes.pipe(res);\n proxyRes.on(\"end\", resolve);\n }\n });\n\n proxyReq.on(\"error\", (err) => {\n reject(err);\n });\n\n if (body) proxyReq.write(body);\n proxyReq.end();\n });\n}\n\nfunction readBody(req: IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n","/**\n * Generated API client functions for v0\n *\n * DO NOT EDIT THIS FILE DIRECTLY\n * This file is auto-generated. To update:\n * 1. Update the OpenAPI spec file\n * 2. Run: pnpm generate\n */\n\nimport type { FetchClient } from \"../lib/fetch-client\";\nimport type { operations } from \"../generated/v0\";\n\n// ============================================================================\n// applicationthemetemplates\n// ============================================================================\n\n/**\n * Lists all theme templates\n * \n *\n * @param client - Fetch client instance\n \n */\nexport async function listThemeTemplates(\n client: FetchClient,\n): Promise<\n operations[\"listThemeTemplates\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_theme_templates`);\n}\n\n/**\n * Creates a theme template\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createThemeTemplate(\n client: FetchClient,\n body: NonNullable<\n operations[\"createThemeTemplate\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createThemeTemplate\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates`, body);\n}\n\n/**\n * List all mysite themes\n * List all mysite themes\n *\n * @param client - Fetch client instance\n \n */\nexport async function listMysiteThemes(\n client: FetchClient,\n): Promise<\n operations[\"listMysiteThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_theme_templates/mysite_themes`);\n}\n\n/**\n * Retrieves a theme template\n * Returns a theme template with details. For section templates whose schema\ndeclares `@theme` or named standalone block references, the response\nincludes an `available_theme_blocks` array with the resolved block schemas\n(name, settings, presets). Private blocks (underscore-prefixed) are excluded\nfrom `@theme` results but included when explicitly referenced by name.\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_theme_templates/${id}`);\n}\n\n/**\n * Updates a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateThemeTemplate(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateThemeTemplate\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/application_theme_templates/${id}`, body);\n}\n\n/**\n * Deletes a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"deleteThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/application_theme_templates/${id}`);\n}\n\n/**\n * Returns all available themeables for theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeTemplateAvailableThemeables(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeTemplateAvailableThemeables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_theme_templates/${id}/available_themeables`,\n );\n}\n\n/**\n * Get available variables for a theme template\n * Get available variables that can be used in the theme template\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeTemplateAvailableVariables(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeTemplateAvailableVariables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_theme_templates/${id}/available_variables`,\n );\n}\n\n/**\n * Clones a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function cloneThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"cloneThemeTemplate\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/clone`);\n}\n\n/**\n * Publishes the template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function publishThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"publishThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/publish`);\n}\n\n/**\n * Renders a page for a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function renderThemeTemplatePage(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"renderThemeTemplatePage\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"renderThemeTemplatePage\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(\n `/api/application_theme_templates/${id}/render_page`,\n body,\n );\n}\n\n/**\n * Renders a section template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function renderThemeTemplateSection(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"renderThemeTemplateSection\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/render_section`);\n}\n\n/**\n * Sets a theme template as default\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function setDefaultThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"setDefaultThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/set_default`);\n}\n\n/**\n * Updates themeable records to be used by the specified template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function updateThemeTemplateThemeables(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"updateThemeTemplateThemeables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.put(`/api/application_theme_templates/${id}/themeables_update`);\n}\n\n// ============================================================================\n// application-themes\n// ============================================================================\n\n/**\n * List application themes\n * Get all application themes with optional filters\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listApplicationThemes(\n client: FetchClient,\n params?: operations[\"listApplicationThemes\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listApplicationThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes`, params);\n}\n\n/**\n * Create an application theme\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createApplicationTheme(\n client: FetchClient,\n body: NonNullable<\n operations[\"createApplicationTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createApplicationTheme\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes`, body);\n}\n\n/**\n * Get current active application theme\n * \n *\n * @param client - Fetch client instance\n \n */\nexport async function getActiveApplicationTheme(\n client: FetchClient,\n): Promise<\n operations[\"getActiveApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes/active`);\n}\n\n/**\n * Import an application theme from zip file\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function importApplicationThemeFromZip(\n client: FetchClient,\n body: NonNullable<\n operations[\"importApplicationThemeFromZip\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"importApplicationThemeFromZip\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/import_zip`, body);\n}\n\n/**\n * Get an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param [params] - params\n */\nexport async function getApplicationTheme(\n client: FetchClient,\n id: string | number,\n params?: operations[\"getApplicationTheme\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"getApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes/${id}`, params);\n}\n\n/**\n * Update an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateApplicationTheme(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateApplicationTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/application_themes/${id}`, body);\n}\n\n/**\n * Delete an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"deleteApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/application_themes/${id}`);\n}\n\n/**\n * Returns available themeables for a given type scoped to the theme's company\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param [params] - params\n */\nexport async function getApplicationThemeAvailableThemeables(\n client: FetchClient,\n id: string | number,\n params?: operations[\"getApplicationThemeAvailableThemeables\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"getApplicationThemeAvailableThemeables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_themes/${id}/available_themeables`,\n params,\n );\n}\n\n/**\n * Clone an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function cloneApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"cloneApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/${id}/clone`);\n}\n\n/**\n * Create a development reference clone of an application theme\n * Creates an isolated development theme while preserving existing DAM and ImageKit references without transferring asset bytes.\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function cloneApplicationThemeForDevelopment(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"cloneApplicationThemeForDevelopment\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"cloneApplicationThemeForDevelopment\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(\n `/api/application_themes/${id}/clone_for_development`,\n body,\n );\n}\n\n/**\n * Import an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function importApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"importApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/${id}/import`);\n}\n\n/**\n * Publishes the theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function publishApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"publishApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/${id}/publish`);\n}\n\n/**\n * Get theme assets\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeAssets(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeAssets\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes/${id}/theme_assets`);\n}\n\n// ============================================================================\n// applicationthemeresources\n// ============================================================================\n\n/**\n * Lists all theme resources\n *\n *\n * @param client - Fetch client instance\n * @param application_theme_id - application_theme_id\n */\nexport async function listThemeResources(\n client: FetchClient,\n application_theme_id: string | number,\n): Promise<\n operations[\"listThemeResources\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_themes/${application_theme_id}/resources`,\n );\n}\n\n/**\n * Updates a theme resource\n *\n *\n * @param client - Fetch client instance\n * @param application_theme_id - application_theme_id\n * @param body - body\n */\nexport async function updateThemeResource(\n client: FetchClient,\n application_theme_id: string | number,\n body: NonNullable<\n operations[\"updateThemeResource\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateThemeResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.put(\n `/api/application_themes/${application_theme_id}/resources`,\n body,\n );\n}\n\n/**\n * Deletes a theme resource\n *\n *\n * @param client - Fetch client instance\n * @param application_theme_id - application_theme_id\n * @param body - body\n */\nexport async function deleteThemeResource(\n client: FetchClient,\n application_theme_id: string | number,\n body: NonNullable<\n operations[\"deleteThemeResource\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"deleteThemeResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(\n `/api/application_themes/${application_theme_id}/resources`,\n { body },\n );\n}\n\n// ============================================================================\n// file-resources\n// ============================================================================\n\n/**\n * Returns a list of file resources\n *\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listFileResources(\n client: FetchClient,\n params?: operations[\"listFileResources\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listFileResources\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/file_resources`, params);\n}\n\n/**\n * Creates a file resource\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createFileResource(\n client: FetchClient,\n body: NonNullable<\n operations[\"createFileResource\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createFileResource\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/file_resources`, body);\n}\n\n/**\n * Creates multiple file resources\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function bulkCreateFileResources(\n client: FetchClient,\n body: NonNullable<\n operations[\"bulkCreateFileResources\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"bulkCreateFileResources\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/file_resources/bulk_create`, body);\n}\n\n/**\n * Deletes multiple file resources\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function bulkDestroyFileResources(\n client: FetchClient,\n body: NonNullable<\n operations[\"bulkDestroyFileResources\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"bulkDestroyFileResources\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/file_resources/bulk_destroy`, { body });\n}\n\n/**\n * Shows a file resource\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function showFileResource(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"showFileResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/file_resources/${id}`);\n}\n\n/**\n * Deletes a file resource\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function destroyFileResource(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"destroyFileResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/file_resources/${id}`);\n}\n\n// ============================================================================\n// root-themes\n// ============================================================================\n\n/**\n * List root themes\n * Get all root themes with optional filters\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listRootThemes(\n client: FetchClient,\n params?: operations[\"listRootThemes\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listRootThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/root_themes`, params);\n}\n\n/**\n * Create a root theme\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createRootTheme(\n client: FetchClient,\n body: NonNullable<\n operations[\"createRootTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createRootTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/root_themes`, body);\n}\n\n/**\n * List company root themes\n * Get all company root themes with optional filters\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listCompanyRootThemes(\n client: FetchClient,\n params?: operations[\"listCompanyRootThemes\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listCompanyRootThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/root_themes/my`, params);\n}\n\n/**\n * Update a root theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateRootTheme(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateRootTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateRootTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/root_themes/${id}`, body);\n}\n\n/**\n * Delete a root theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteRootTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"deleteRootTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/root_themes/${id}`);\n}\n\n/**\n * Update a root theme status\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateRootThemeStatus(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateRootThemeStatus\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateRootThemeStatus\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/root_themes/${id}/status`, body);\n}\n\n// ============================================================================\n// theme-region-rules\n// ============================================================================\n\n/**\n * List theme region rules\n * Retrieve a list of theme region rules for the current company\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listThemeRegionRules(\n client: FetchClient,\n params?: operations[\"listThemeRegionRules\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listThemeRegionRules\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/theme_region_rules`, params);\n}\n\n/**\n * Create theme region rule\n * Create a new theme region rule\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createThemeRegionRule(\n client: FetchClient,\n body: NonNullable<\n operations[\"createThemeRegionRule\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createThemeRegionRule\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/theme_region_rules`, body);\n}\n\n/**\n * Show theme region rule\n * Retrieve a specific theme region rule\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeRegionRule(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeRegionRule\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/theme_region_rules/${id}`);\n}\n\n/**\n * Update theme region rule\n * Update an existing theme region rule\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateThemeRegionRule(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateThemeRegionRule\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateThemeRegionRule\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/theme_region_rules/${id}`, body);\n}\n\n/**\n * Delete theme region rule\n * Delete a theme region rule\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteThemeRegionRule(\n client: FetchClient,\n id: string | number,\n): Promise<void> {\n return client.delete(`/api/theme_region_rules/${id}`);\n}\n","import { isApiError } from \"@fluid-app/themes-api-client\";\n\n// Name of the bundled skill that walks the caller through migrating\n// per-template `styles.css` files to theme-level assets. See\n// `skills/template-stylesheet-to-asset-migration/SKILL.md` in this\n// package.\nexport const STYLESHEET_MIGRATION_SKILL =\n \"template-stylesheet-to-asset-migration\";\n\n// Detect the backend's 422 rejection for legacy stylesheet keys\n// (`ApplicationThemeResources::UpdateAction#stylesheet_rejected_response`\n// on the Rails side). Both the message and the errors payload identify\n// the rejection uniquely; match on either so a future wording tweak on\n// one surface does not silently drop the hint.\nfunction isStylesheetKeyRejection(error: {\n status: number;\n message: string;\n data: unknown;\n}): boolean {\n if (error.status !== 422) return false;\n if (/stylesheet.*no longer accepted/i.test(error.message)) return true;\n if (error.data && typeof error.data === \"object\") {\n const resourceErrors = (\n error.data as { application_theme_resource?: { key?: unknown } }\n ).application_theme_resource;\n if (\n resourceErrors &&\n typeof resourceErrors.key === \"string\" &&\n /stylesheet.*no longer accepted/i.test(resourceErrors.key)\n ) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Extract a human-readable message from a caught error. ApiError's default\n * `toString` prefixes with `ApiError:` and the class name; this returns the\n * API's own `error_message` verbatim so the CLI surfaces messages like\n * \"This stylesheet key is no longer accepted. Upload stylesheets as\n * theme-level assets.\" directly.\n *\n * When the error is the legacy stylesheet key rejection specifically,\n * a hint is appended pointing to the bundled migration skill — the\n * caller needs to move the per-template `styles.css` bytes to `assets/`\n * and stop pushing the deprecated column key.\n */\nexport function formatError(e: unknown): string {\n if (isApiError(e)) {\n const status = e.status ? ` [${e.status}]` : \"\";\n const hint = isStylesheetKeyRejection(e)\n ? `\\n ↳ Run \\`fluid theme skills install\\` — the bundled \\`${STYLESHEET_MIGRATION_SKILL}\\` skill can help you migrate this to a theme-level asset.`\n : \"\";\n return `${e.message}${status}${hint}`;\n }\n if (e instanceof Error) return e.message;\n return String(e);\n}\n","import { relative, sep } from \"node:path\";\nimport chokidar from \"chokidar\";\nimport type { ThemeRoot } from \"../root.js\";\nimport type { ThemeFile } from \"../file.js\";\nimport { formatError } from \"../format-error.js\";\n\nexport type FileChangeHandler = (\n modified: ThemeFile[],\n added: ThemeFile[],\n removed: ThemeFile[],\n /** When the filesystem event arrived, not when the handler got to run.\n * Handlers are serialized behind awaited uploads, so a queued event can\n * start long after it happened — anything deciding where one edit ends and\n * the next begins has to use this, or it measures upload duration. */\n arrivedAt: number,\n) => Promise<void>;\n\nfunction relativeThemePath(root: ThemeRoot, filePath: string): string {\n return relative(root.root, filePath).split(sep).join(\"/\");\n}\n\nexport function watchTheme(\n root: ThemeRoot,\n handler: FileChangeHandler,\n): () => Promise<void> {\n const watcher = chokidar.watch(root.root, {\n ignoreInitial: true,\n ignored: (filePath: string) => {\n if (filePath.includes(\"node_modules\")) return true;\n try {\n const rel = relativeThemePath(root, filePath);\n const basename = rel.split(/[\\\\/]/).pop() ?? \"\";\n return basename.startsWith(\".\") || root.ignore.ignore(rel);\n } catch {\n return false;\n }\n },\n persistent: true,\n awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 },\n });\n\n let pending = Promise.resolve();\n const enqueue = (fn: () => Promise<void>) => {\n // The change handler has its own internal try/catch around\n // per-file uploads/deletes, but anything that throws outside\n // that (e.g. `root.file()`, `validateSchema()`) must still be\n // surfaced — a bare `.catch(() => {})` here previously swallowed\n // it and left watch mode silently stuck.\n pending = pending.then(fn).catch((e) => {\n console.error(` [Watcher] change handling failed: ${formatError(e)}`);\n });\n };\n\n watcher.on(\"change\", (filePath) => {\n const rel = relativeThemePath(root, filePath);\n if (!root.isResourcePath(filePath) || root.ignore.ignore(rel)) return;\n const arrivedAt = Date.now();\n enqueue(() => handler([root.file(filePath)], [], [], arrivedAt));\n });\n\n watcher.on(\"add\", (filePath) => {\n const rel = relativeThemePath(root, filePath);\n if (!root.isResourcePath(filePath) || root.ignore.ignore(rel)) return;\n const arrivedAt = Date.now();\n enqueue(() => handler([], [root.file(filePath)], [], arrivedAt));\n });\n\n watcher.on(\"unlink\", (filePath) => {\n const rel = relativeThemePath(root, filePath);\n if (!root.isResourcePath(filePath) || root.ignore.ignore(rel)) return;\n const arrivedAt = Date.now();\n enqueue(() => handler([], [], [root.file(filePath)], arrivedAt));\n });\n\n return () => watcher.close();\n}\n","import { createHash } from \"node:crypto\";\nimport { lstatSync, readFileSync, readdirSync } from \"node:fs\";\nimport { dirname, join, relative, resolve, sep } from \"node:path\";\n\nconst METADATA = [\".fluid-assets.json\", \".fluid-theme.json\", \".fluidignore\"];\n\nexport class BackgroundPullChangedError extends Error {\n constructor() {\n super(\n \"Local files changed during background pull; remaining updates skipped.\",\n );\n }\n}\n\n/** Keep the preflight snapshot across asynchronous download/merge work. Each\n * mutation checks its original bytes synchronously and records only our writes.\n * This prevents an edit made after preflight from becoming a merge input or\n * being overwritten by a deferred write, asset cleanup, or metadata update. */\nexport class BackgroundPullGuard {\n private readonly root: string;\n private readonly expected: Map<string, string | null>;\n\n constructor(root: string) {\n this.root = resolve(root);\n this.expected = this.snapshot();\n }\n\n assertUnchanged(): void {\n const current = this.snapshot();\n for (const key of new Set([...current.keys(), ...this.expected.keys()])) {\n if ((current.get(key) ?? null) !== (this.expected.get(key) ?? null)) {\n throw new BackgroundPullChangedError();\n }\n }\n }\n\n mutate(keys: readonly string[], action: () => void): void {\n for (const key of keys) {\n if (this.fingerprint(key) !== (this.expected.get(key) ?? null)) {\n throw new BackgroundPullChangedError();\n }\n }\n action();\n for (const key of keys) this.expected.set(key, this.fingerprint(key));\n }\n\n private fingerprint(key: string): string | null {\n const path = resolve(this.root, key);\n if (!path.startsWith(this.root + sep))\n throw new BackgroundPullChangedError();\n // Fail closed for a parent replaced by a symlink while awaiting Git/network.\n for (let cursor = path; ; cursor = dirname(cursor)) {\n try {\n if (lstatSync(cursor).isSymbolicLink())\n throw new BackgroundPullChangedError();\n } catch (error) {\n if (\n !(\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n error.code === \"ENOENT\"\n )\n )\n throw error;\n }\n if (cursor === this.root) break;\n }\n try {\n return createHash(\"sha256\").update(readFileSync(path)).digest(\"hex\");\n } catch (error) {\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n error.code === \"ENOENT\"\n )\n return null;\n throw error;\n }\n }\n\n private snapshot(): Map<string, string | null> {\n const result = new Map<string, string | null>();\n const visit = (directory: string): void => {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n if (entry.name.startsWith(\".\") || entry.name === \"node_modules\")\n continue;\n if (entry.isSymbolicLink()) throw new BackgroundPullChangedError();\n const path = join(directory, entry.name);\n if (entry.isDirectory()) visit(path);\n else {\n const key = relative(this.root, path).split(sep).join(\"/\");\n result.set(key, this.fingerprint(key));\n }\n }\n };\n visit(this.root);\n for (const key of METADATA) result.set(key, this.fingerprint(key));\n return result;\n }\n}\n","import { normalizeThemeResourceKey } from \"./resource-key.js\";\n\nexport class CaseCollisionError extends Error {\n constructor(readonly collisions: readonly (readonly string[])[]) {\n super(\n `Theme contains paths that differ only by letter case and cannot be synchronized safely:\\n${collisions\n .map((paths) => ` ${paths.join(\", \")}`)\n .join(\"\\n\")}`,\n );\n this.name = \"CaseCollisionError\";\n }\n}\n\n/** Refuse paths a case-insensitive checkout cannot represent independently. */\nexport function assertNoCaseCollisions(paths: readonly string[]): void {\n const pathsByFoldedKey = new Map<string, Set<string>>();\n for (const rawPath of paths) {\n const path = normalizeThemeResourceKey(rawPath);\n const foldedKey = path.toLowerCase();\n const matchingPaths = pathsByFoldedKey.get(foldedKey) ?? new Set<string>();\n matchingPaths.add(path);\n pathsByFoldedKey.set(foldedKey, matchingPaths);\n }\n\n const collisions = [...pathsByFoldedKey.values()]\n .filter((matchingPaths) => matchingPaths.size > 1)\n .map((matchingPaths) => [...matchingPaths].sort())\n .sort(([left = \"\"], [right = \"\"]) => left.localeCompare(right));\n\n if (collisions.length > 0) throw new CaseCollisionError(collisions);\n}\n","import { createHash, randomBytes } from \"node:crypto\";\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\nconst MANIFEST_FILE = \".fluid-assets.json\";\nconst MANIFEST_VERSION = 1;\n\n/**\n * A tiny shadow-repo placeholder for a manifest-backed asset. It preserves\n * the path as a deletion baseline without retaining the asset bytes locally.\n */\nexport const MANAGED_ASSET_SHADOW_SENTINEL = \"fluid-managed-asset\\n\";\n\nexport interface ThemeAssetLink {\n /** Theme that last supplied this asset reference. */\n sourceThemeId: number;\n /** SHA-256 checksum of the source resource, when the API provides one. */\n checksum?: string;\n /** ImageKit URL saved into themes that consume this asset. */\n url?: string;\n /** MIME type required to create a URL-backed FileResource. */\n contentType?: string;\n /** File size required to create a URL-backed FileResource. */\n contentSize?: number;\n /** Optional display metadata copied with the ImageKit URL. */\n previewImageUrl?: string;\n altText?: string;\n handle?: string;\n /** A dev-only source that must survive pull until a push makes it durable. */\n pending?: boolean;\n /** DAM asset identity retained as provenance for CLI-uploaded assets. */\n damAssetCode?: string;\n}\n\ninterface ThemeAssetManifestDocument {\n version: number;\n assets: Record<string, ThemeAssetLink>;\n}\n\n/**\n * Tracks binary theme assets that deliberately live only on the server.\n *\n * The manifest is a dotfile so it is excluded from theme uploads and file\n * watching. It is written before the corresponding local file is removed,\n * which prevents `delete: true` from mistaking the removed byte source for a\n * request to delete its remote FileResource.\n */\nexport class ThemeAssetManifest {\n private assets: Record<string, ThemeAssetLink>;\n private readonly path: string;\n\n constructor(themeRoot: string) {\n this.path = join(themeRoot, MANIFEST_FILE);\n this.assets = readDocument(this.path).assets;\n }\n\n reload(): void {\n this.assets = readDocument(this.path).assets;\n }\n\n keys(): string[] {\n return Object.keys(this.assets);\n }\n\n entries(): Array<[string, ThemeAssetLink]> {\n return Object.entries(this.assets).map(([key, link]) => [\n key,\n copyLink(link),\n ]);\n }\n\n /**\n * Stable digest of the URL references represented by this manifest.\n * Pull baselines exclude pending entries because those belong only to an\n * existing dev target and are deliberately absent from the pulled source.\n */\n fingerprint(opts: { excludePending?: boolean } = {}): string {\n const entries = this.entries()\n .filter(([, link]) => !opts.excludePending || !link.pending)\n .toSorted(([left], [right]) =>\n left < right ? -1 : left > right ? 1 : 0,\n );\n return createHash(\"sha256\").update(JSON.stringify(entries)).digest(\"hex\");\n }\n\n has(key: string): boolean {\n return this.assets[key] !== undefined;\n }\n\n get(key: string): ThemeAssetLink | undefined {\n const link = this.assets[key];\n return link ? copyLink(link) : undefined;\n }\n\n set(key: string, link: ThemeAssetLink): void {\n if (!isThemeAssetKey(key) || !isThemeAssetLink(link)) {\n throw new Error(`invalid asset entry for ${key}`);\n }\n this.assets[key] = copyLink(link);\n }\n\n delete(key: string): void {\n delete this.assets[key];\n }\n\n write(): void {\n const document: ThemeAssetManifestDocument = {\n version: MANIFEST_VERSION,\n assets: copyAssets(this.assets),\n };\n const tempPath = `${this.path}.${randomBytes(6).toString(\"hex\")}.tmp`;\n\n try {\n mkdirSync(dirname(this.path), { recursive: true });\n writeFileSync(tempPath, JSON.stringify(document, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n renameSync(tempPath, this.path);\n } catch (error) {\n try {\n unlinkSync(tempPath);\n } catch {\n // The temporary file may not have been written yet.\n }\n throw error;\n }\n }\n}\n\nfunction readDocument(path: string): ThemeAssetManifestDocument {\n if (!existsSync(path)) return emptyDocument();\n\n try {\n return parseDocument(JSON.parse(readFileSync(path, \"utf-8\")));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Could not read ${MANIFEST_FILE}: ${message}`);\n }\n}\n\nfunction parseDocument(value: unknown): ThemeAssetManifestDocument {\n if (!isRecord(value) || value[\"version\"] !== MANIFEST_VERSION) {\n throw new Error(`expected version ${MANIFEST_VERSION}`);\n }\n\n const rawAssets = value[\"assets\"];\n if (!isRecord(rawAssets)) throw new Error(\"expected an assets object\");\n\n const assets: Record<string, ThemeAssetLink> = {};\n for (const [key, rawLink] of Object.entries(rawAssets)) {\n if (!isThemeAssetKey(key) || !isThemeAssetLink(rawLink)) {\n throw new Error(`invalid asset entry for ${key}`);\n }\n assets[key] = copyLink(rawLink);\n }\n\n return { version: MANIFEST_VERSION, assets };\n}\n\nfunction emptyDocument(): ThemeAssetManifestDocument {\n return { version: MANIFEST_VERSION, assets: {} };\n}\n\nfunction copyAssets(\n assets: Record<string, ThemeAssetLink>,\n): Record<string, ThemeAssetLink> {\n return Object.fromEntries(\n Object.entries(assets).map(([key, link]) => [key, copyLink(link)]),\n );\n}\n\nfunction copyLink(link: ThemeAssetLink): ThemeAssetLink {\n return {\n sourceThemeId: link.sourceThemeId,\n ...(typeof link.checksum === \"string\" ? { checksum: link.checksum } : {}),\n ...(typeof link.url === \"string\" ? { url: link.url } : {}),\n ...(typeof link.contentType === \"string\"\n ? { contentType: link.contentType }\n : {}),\n ...(typeof link.contentSize === \"number\"\n ? { contentSize: link.contentSize }\n : {}),\n ...(typeof link.previewImageUrl === \"string\"\n ? { previewImageUrl: link.previewImageUrl }\n : {}),\n ...(typeof link.altText === \"string\" ? { altText: link.altText } : {}),\n ...(typeof link.handle === \"string\" ? { handle: link.handle } : {}),\n ...(link.pending === true ? { pending: true } : {}),\n ...(typeof link.damAssetCode === \"string\"\n ? { damAssetCode: link.damAssetCode }\n : {}),\n };\n}\n\nfunction isThemeAssetLink(value: unknown): value is ThemeAssetLink {\n return (\n isRecord(value) &&\n typeof value[\"sourceThemeId\"] === \"number\" &&\n Number.isInteger(value[\"sourceThemeId\"]) &&\n value[\"sourceThemeId\"] > 0 &&\n (value[\"checksum\"] === undefined ||\n (typeof value[\"checksum\"] === \"string\" &&\n value[\"checksum\"].length > 0)) &&\n (value[\"url\"] === undefined ||\n (typeof value[\"url\"] === \"string\" && value[\"url\"].length > 0)) &&\n (value[\"contentType\"] === undefined ||\n (typeof value[\"contentType\"] === \"string\" &&\n value[\"contentType\"].length > 0)) &&\n (value[\"contentSize\"] === undefined ||\n (typeof value[\"contentSize\"] === \"number\" &&\n Number.isInteger(value[\"contentSize\"]) &&\n value[\"contentSize\"] > 0)) &&\n (value[\"previewImageUrl\"] === undefined ||\n (typeof value[\"previewImageUrl\"] === \"string\" &&\n value[\"previewImageUrl\"].length > 0)) &&\n (value[\"altText\"] === undefined || typeof value[\"altText\"] === \"string\") &&\n (value[\"handle\"] === undefined ||\n (typeof value[\"handle\"] === \"string\" && value[\"handle\"].length > 0)) &&\n (value[\"pending\"] === undefined || typeof value[\"pending\"] === \"boolean\") &&\n (value[\"damAssetCode\"] === undefined ||\n (typeof value[\"damAssetCode\"] === \"string\" &&\n value[\"damAssetCode\"].length > 0))\n );\n}\n\nexport function isThemeAssetKey(key: string): boolean {\n if (key.includes(\"\\\\\") || key.includes(\"\\0\")) return false;\n\n const segments = key.split(\"/\");\n return (\n segments[0] === \"assets\" &&\n segments.length === 2 &&\n segments[1] !== undefined &&\n segments[1].length > 0 &&\n segments[1] !== \".\" &&\n segments[1] !== \"..\"\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n","// Resource keys that the backend can hide from the resources index\n// when `STYLESHEET_STRICT_INPUT` is enabled on the owning company —\n// theme-level `styles.css` / `global_styles.css` and per-template\n// composite `{type}/{name}/styles.css`. See `Themes::Resource.find_by`\n// + `Themes::Theme#theme_stylesheet_columns_hidden?` /\n// `Themes::Template#stylesheet_column_hidden?` on the Rails side.\n//\n// A pull that used the \"delete files missing on remote\" behavior would\n// otherwise wipe a merchant's local stylesheets the first time the\n// flag flips on — the resource just stops appearing in the index, not\n// the file itself. Skip these keys from the delete pass so local\n// content stays intact regardless of the flag state.\n\nconst STYLESHEET_KEY_PATTERN =\n /^(styles\\.css|global_styles\\.css|[^/]+\\/[^/]+\\/styles\\.css)$/;\n\nexport function isStylesheetKey(key: string): boolean {\n return STYLESHEET_KEY_PATTERN.test(key);\n}\n","import {\n BackgroundPullChangedError,\n type BackgroundPullGuard,\n} from \"./background-pull-guard.js\";\nimport { unlinkSync } from \"node:fs\";\nimport { sep } from \"node:path\";\nimport {\n isApiError,\n themes,\n type components,\n} from \"@fluid-app/themes-api-client\";\nimport type { ApiClient } from \"../api.js\";\nimport { formatError } from \"./format-error.js\";\nimport type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport { assertNoCaseCollisions } from \"./case-collisions.js\";\nimport {\n isThemeAssetKey,\n ThemeAssetManifest,\n type ThemeAssetLink,\n} from \"./asset-manifest.js\";\nimport { isStylesheetKey } from \"./stylesheet-keys.js\";\nimport type {\n DevRemoteState,\n RemoteResourceState,\n} from \"./dev-remote-baseline.js\";\n\ntype RemoteResource = components[\"schemas\"][\"ApplicationThemeResource\"];\ninterface RemoteAssetMetadata {\n url: string;\n contentType: string;\n contentSize: number;\n previewImageUrl?: string;\n altText?: string;\n handle?: string;\n}\n\ninterface ManagedAssetPlan {\n key: string;\n link: ThemeAssetLink;\n targetResource?: RemoteResource;\n metadata?: RemoteAssetMetadata;\n}\n\ntype UploadedBinaryResource = RemoteResource & {\n damAssetCode?: string;\n assetMetadata?: RemoteAssetMetadata;\n};\n\nconst ASSET_REFERENCE_CONCURRENCY = 6;\n\nexport interface SyncResult {\n uploaded: number;\n downloaded: number;\n linked: number;\n deleted: number;\n errors: string[];\n validationFailed: boolean;\n}\n\n/**\n * Server rejected the push because the CLI's `base_sha` no longer\n * matches the theme's current `content_version_sha` (someone else\n * wrote to the theme since our last pull). Callers surface a\n * \"pull first\" message; no partial state has been written when this\n * throws from the preflight, and the per-file variant preserves the\n * atomicity guarantee mid-loop by short-circuiting the remaining\n * files on the first conflicting response.\n */\nexport class PushConflictError extends Error {\n constructor(public readonly remoteSha: string | null) {\n super(\n remoteSha\n ? `Your local is behind the server. Server is at ${remoteSha}; local is stale.`\n : \"Your local is behind the server.\",\n );\n this.name = \"PushConflictError\";\n }\n}\n\nexport class Syncer {\n private checksumIndex = new Map<string, string>();\n private rawRemoteResources = new Map<string, RemoteResource>();\n private remoteResourceGroups = new Map<string, RemoteResource[]>();\n private remoteResourceIndex = new Map<string, RemoteResource>();\n private remoteIndexesDirty = false;\n private remoteResourcesLoaded = false;\n private lastKnownRemoteSha: string | null = null;\n private assetManifestInstance: ThemeAssetManifest | undefined;\n\n constructor(\n private api: ApiClient,\n private themeId: number,\n private themeRoot: ThemeRoot,\n assetManifest?: ThemeAssetManifest,\n ) {\n this.assetManifestInstance = assetManifest;\n }\n\n private get assetManifest(): ThemeAssetManifest {\n this.assetManifestInstance ??= new ThemeAssetManifest(this.themeRoot.root);\n return this.assetManifestInstance;\n }\n\n // ─── Checksum Management ──────────────────────────────────────────────────\n\n async fetchChecksums(): Promise<void> {\n // `content_version_sha` on the resources index is a Phase 003a\n // server addition — older servers don't emit it. Cast through\n // `unknown` because the typed API client hasn't been regenerated\n // against the new OpenAPI spec yet; regeneration is a follow-up.\n const body = (await themes.listThemeResources(\n this.api,\n this.themeId,\n )) as unknown as {\n application_theme_resources?: RemoteResource[];\n content_version_sha?: string;\n };\n this.updateChecksums(body.application_theme_resources ?? []);\n this.lastKnownRemoteSha = body.content_version_sha ?? null;\n this.remoteResourcesLoaded = true;\n }\n\n /**\n * Server's `content_version_sha` captured on the last `fetchChecksums()`\n * or `downloadAll()`. `null` when talking to a pre-003a server.\n */\n remoteSha(): string | null {\n return this.lastKnownRemoteSha;\n }\n\n private updateChecksums(resources: RemoteResource[]): void {\n assertNoCaseCollisions(\n resources.flatMap((resource) => (resource.key ? [resource.key] : [])),\n );\n this.rawRemoteResources.clear();\n this.remoteResourceGroups.clear();\n for (const resource of resources) {\n if (!resource.key) continue;\n\n this.rawRemoteResources.set(resource.key, resource);\n const group = this.remoteResourceGroups.get(resource.key) ?? [];\n group.push(resource);\n this.remoteResourceGroups.set(resource.key, group);\n }\n this.remoteIndexesDirty = true;\n }\n\n private setRemoteResource(resource: RemoteResource): void {\n if (!resource.key) return;\n this.rawRemoteResources.set(resource.key, resource);\n this.remoteResourceGroups.set(resource.key, [resource]);\n this.remoteIndexesDirty = true;\n }\n\n private removeRemoteResource(relativePath: string): void {\n this.rawRemoteResources.delete(relativePath);\n this.remoteResourceGroups.delete(relativePath);\n this.remoteIndexesDirty = true;\n }\n\n // Rebuilding on every setRemoteResource/removeRemoteResource made bulk\n // upload loops O(n²) over the remote resource map, so mutations only mark\n // the indexes dirty and the next read rebuilds once.\n private get checksums(): Map<string, string> {\n if (this.remoteIndexesDirty) this.rebuildRemoteIndexes();\n return this.checksumIndex;\n }\n\n private get remoteResources(): Map<string, RemoteResource> {\n if (this.remoteIndexesDirty) this.rebuildRemoteIndexes();\n return this.remoteResourceIndex;\n }\n\n private rebuildRemoteIndexes(): void {\n this.remoteIndexesDirty = false;\n this.checksumIndex.clear();\n this.remoteResourceIndex.clear();\n\n for (const [key, resource] of this.rawRemoteResources) {\n // The backend may return both a legacy key and its .liquid equivalent.\n // Preserve the explicit .liquid resource when both are present.\n if (this.rawRemoteResources.has(`${key}.liquid`)) continue;\n\n this.remoteResourceIndex.set(key, resource);\n if (resource.checksum) this.checksumIndex.set(key, resource.checksum);\n }\n }\n\n hasChanged(file: ThemeFile): boolean {\n return file.checksum() !== this.checksums.get(file.relativePath);\n }\n\n remoteKeys(): string[] {\n return [...this.remoteResources.keys()];\n }\n\n /** A null-content resource has no possible working-tree counterpart. */\n private canDeleteRemoteResource(key: string): boolean {\n const resource = this.remoteResources.get(key);\n return resource?.content != null || isManagedAssetResource(resource);\n }\n\n /** Snapshot of remote checksums (key → sha256). Available after fetchChecksums() or downloadAll(). */\n remoteChecksums(): Record<string, string> {\n return Object.fromEntries(this.checksums);\n }\n\n /** URL-backed assets keyed by their exact theme resource path. */\n remoteAssetUrls(): Record<string, string> {\n const urls: Record<string, string> = {};\n for (const [key, resource] of this.remoteResources) {\n if (!isManagedAssetResource(resource)) continue;\n const url = resource.url;\n if (typeof url === \"string\" && url.length > 0) urls[key] = url;\n }\n return urls;\n }\n\n /** Compact, complete resource state paired with its acknowledged dev SHA. */\n devRemoteState(assetManifestSha: string): DevRemoteState | null {\n if (!this.remoteResourcesLoaded || !this.lastKnownRemoteSha) return null;\n\n return {\n themeId: this.themeId,\n remoteSha: this.lastKnownRemoteSha,\n assetManifestSha,\n resources: [...this.remoteResourceGroups.values()]\n .flat()\n .map(remoteResourceState),\n };\n }\n\n useDevRemoteState(state: DevRemoteState): void {\n if (state.themeId !== this.themeId) {\n throw new Error(\n `Dev remote state belongs to theme #${state.themeId}, not #${this.themeId}`,\n );\n }\n this.updateChecksums(state.resources.map(remoteResourceFromState));\n this.lastKnownRemoteSha = state.remoteSha;\n this.remoteResourcesLoaded = true;\n }\n\n private async ensureRemoteResourcesLoaded(): Promise<void> {\n if (!this.remoteResourcesLoaded) await this.fetchChecksums();\n }\n\n /**\n * Adds URL-backed FileResources for manifest assets without transferring\n * their bytes. The target stores the source asset's ImageKit URL.\n */\n async linkManagedAssets(opts: { replace?: boolean } = {}): Promise<number> {\n this.assetManifest.reload();\n await this.ensureRemoteResourcesLoaded();\n\n const plans: ManagedAssetPlan[] = [];\n for (const [key, link] of this.assetManifest.entries()) {\n if (this.themeRoot.ignore.ignore(key)) continue;\n // A local binary is the recovery source after a failed metadata pull or\n // an intentional replacement. Let the normal upload path handle it;\n // never recreate the older manifest URL over those bytes first.\n if (this.themeRoot.file(key).exists) continue;\n\n const targetResource =\n this.managedAssetResourceForLink(key, link) ??\n this.remoteResources.get(key);\n if (\n targetResource &&\n !this.managedAssetNeedsRefresh(targetResource, link)\n ) {\n continue;\n }\n if (targetResource && !opts.replace) continue;\n\n const metadata = assetMetadataFromLink(link);\n plans.push({\n key,\n link,\n ...(targetResource ? { targetResource } : {}),\n ...(metadata ? { metadata } : {}),\n });\n }\n\n if (plans.length > 0) {\n await this.resolveAssetMetadata(plans);\n await this.createAssetReferences(plans);\n await this.fetchChecksums();\n }\n if (opts.replace && (await this.pruneDuplicateManagedAssetReferences())) {\n await this.fetchChecksums();\n }\n this.ensureManagedAssetsAreResolved();\n\n return plans.length;\n }\n\n private async resolveAssetMetadata(plans: ManagedAssetPlan[]): Promise<void> {\n const bySourceTheme = new Map<number, ManagedAssetPlan[]>();\n for (const plan of plans) {\n if (plan.metadata) continue;\n const sourcePlans = bySourceTheme.get(plan.link.sourceThemeId) ?? [];\n sourcePlans.push(plan);\n bySourceTheme.set(plan.link.sourceThemeId, sourcePlans);\n }\n\n let manifestChanged = false;\n for (const [sourceThemeId, sourcePlans] of bySourceTheme) {\n let sourceAssets: Map<string, RemoteAssetMetadata>;\n try {\n sourceAssets = await this.fetchThemeAssetMetadata(sourceThemeId);\n } catch (error) {\n throw new Error(\n `Could not read asset metadata from theme #${sourceThemeId}: ${formatError(error)}`,\n );\n }\n\n for (const plan of sourcePlans) {\n const sourceMetadata = sourceAssets.get(assetFilename(plan.key));\n if (!sourceMetadata) {\n throw new Error(\n `Could not find usable metadata for ${plan.key} in theme #${sourceThemeId}`,\n );\n }\n\n // Preserve a pulled URL while enriching an older manifest. A remote\n // source update must not silently change this checkout's asset URL.\n const metadata: RemoteAssetMetadata = {\n ...sourceMetadata,\n ...(typeof plan.link.url === \"string\" ? { url: plan.link.url } : {}),\n };\n plan.metadata = metadata;\n this.assetManifest.set(plan.key, {\n ...plan.link,\n ...metadata,\n });\n manifestChanged = true;\n }\n }\n\n if (manifestChanged) this.assetManifest.write();\n }\n\n private async fetchThemeAssetMetadata(\n sourceThemeId: number,\n ): Promise<Map<string, RemoteAssetMetadata>> {\n const body = await themes.getThemeAssets(this.api, sourceThemeId);\n if (!isRecord(body) || !Array.isArray(body[\"file_resources\"])) {\n throw new Error(\"Theme assets response did not include file_resources\");\n }\n\n const assets = new Map<string, RemoteAssetMetadata>();\n for (const value of body[\"file_resources\"]) {\n const asset = parseThemeAssetMetadata(value);\n if (asset) assets.set(asset.filename, asset.metadata);\n }\n return assets;\n }\n\n private async createAssetReferences(\n plans: ManagedAssetPlan[],\n ): Promise<void> {\n const errors: string[] = [];\n let nextPlan = 0;\n\n const worker = async () => {\n while (nextPlan < plans.length) {\n const plan = plans[nextPlan];\n nextPlan += 1;\n if (!plan || !plan.metadata) continue;\n\n try {\n await this.createAssetReference(plan);\n } catch (error) {\n errors.push(`${plan.key}: ${formatError(error)}`);\n }\n }\n };\n\n await Promise.all(\n Array.from(\n { length: Math.min(ASSET_REFERENCE_CONCURRENCY, plans.length) },\n worker,\n ),\n );\n\n if (errors.length > 0) {\n throw new Error(\n `Could not save ${errors.length} ImageKit URL reference(s) (this requires File Resources update access): ${errors.join(\"; \")}`,\n );\n }\n }\n\n private async createAssetReference(plan: ManagedAssetPlan): Promise<void> {\n const metadata = plan.metadata;\n if (!metadata) throw new Error(\"asset metadata was not resolved\");\n\n const targetResourceId = plan.targetResource?.resource_id;\n if (plan.targetResource && typeof targetResourceId !== \"number\") {\n throw new Error(\"existing target asset has no resource ID\");\n }\n\n const body = await themes.createFileResource(this.api, {\n file_resource: {\n url: metadata.url,\n filename: assetFilename(plan.key),\n content_type: metadata.contentType,\n content_size: metadata.contentSize,\n ...(metadata.previewImageUrl\n ? { preview_image_url: metadata.previewImageUrl }\n : {}),\n ...(metadata.altText !== undefined\n ? { alt_text: metadata.altText }\n : {}),\n ...(metadata.handle ? { handle: metadata.handle } : {}),\n relateable_id: this.themeId,\n relateable_type: \"ApplicationTheme\",\n },\n });\n\n const createdResourceId = createdFileResourceId(body);\n if (!createdResourceId) {\n throw new Error(\"create response did not include a FileResource ID\");\n }\n\n if (typeof targetResourceId !== \"number\") return;\n\n try {\n await themes.destroyFileResource(this.api, targetResourceId);\n } catch (error) {\n if (isNotFoundError(error)) return;\n try {\n await themes.destroyFileResource(this.api, createdResourceId);\n } catch {\n // Keep the original error; a later sync can repair any duplicate.\n }\n throw error;\n }\n }\n\n /**\n * Returns every resource for an exact logical key. A legacy `foo` resource\n * is hidden when `foo.liquid` exists, matching the normal remote index.\n */\n private resourcesForLogicalKey(key: string): RemoteResource[] {\n if (this.rawRemoteResources.has(`${key}.liquid`)) return [];\n return this.remoteResourceGroups.get(key) ?? [];\n }\n\n private managedAssetResourceForLink(\n key: string,\n link: ThemeAssetLink,\n ): RemoteResource | undefined {\n return this.resourcesForLogicalKey(key).find(\n (resource) =>\n isManagedAssetResource(resource) &&\n !this.managedAssetNeedsRefresh(resource, link),\n );\n }\n\n /** Make interrupted reference replacement converge to one FileResource. */\n private async pruneDuplicateManagedAssetReferences(): Promise<boolean> {\n const resourcesToDelete: Array<{ key: string; resourceId: number }> = [];\n\n for (const [key, link] of this.assetManifest.entries()) {\n if (this.themeRoot.ignore.ignore(key) || !link.url) continue;\n\n const resources = this.resourcesForLogicalKey(key).filter(\n isManagedAssetResource,\n );\n if (resources.length < 2) continue;\n\n const matchingResources = resources.filter(\n (resource) => resource.url === link.url,\n );\n if (matchingResources.length === 0) continue;\n\n const keeper = this.lowestResourceId(matchingResources, key);\n const keeperId = this.resourceIdForAssetReference(keeper, key);\n for (const resource of resources) {\n const resourceId = this.resourceIdForAssetReference(resource, key);\n if (resourceId !== keeperId)\n resourcesToDelete.push({ key, resourceId });\n }\n }\n\n if (resourcesToDelete.length === 0) return false;\n\n const errors: string[] = [];\n let nextResource = 0;\n const worker = async () => {\n while (nextResource < resourcesToDelete.length) {\n const resource = resourcesToDelete[nextResource];\n nextResource += 1;\n if (!resource) continue;\n\n try {\n await themes.destroyFileResource(this.api, resource.resourceId);\n } catch (error) {\n if (!isNotFoundError(error)) {\n errors.push(`${resource.key}: ${formatError(error)}`);\n }\n }\n }\n };\n\n await Promise.all(\n Array.from(\n {\n length: Math.min(\n ASSET_REFERENCE_CONCURRENCY,\n resourcesToDelete.length,\n ),\n },\n worker,\n ),\n );\n\n if (errors.length > 0) {\n throw new Error(\n `Could not remove ${errors.length} duplicate ImageKit URL reference(s): ${errors.join(\"; \")}`,\n );\n }\n\n return true;\n }\n\n private lowestResourceId(\n resources: RemoteResource[],\n key: string,\n ): RemoteResource {\n let lowest = resources[0];\n if (!lowest) throw new Error(`No asset resources found for ${key}`);\n\n let lowestId = this.resourceIdForAssetReference(lowest, key);\n for (const resource of resources.slice(1)) {\n const resourceId = this.resourceIdForAssetReference(resource, key);\n if (resourceId < lowestId) {\n lowest = resource;\n lowestId = resourceId;\n }\n }\n return lowest;\n }\n\n private resourceIdForAssetReference(\n resource: RemoteResource,\n key: string,\n ): number {\n const resourceId = positiveInteger(resource.resource_id);\n if (!resourceId) {\n throw new Error(`Existing target asset has no resource ID: ${key}`);\n }\n return resourceId;\n }\n\n /** Makes this target the provenance source for assets it now resolves. */\n repointManagedAssetsToCurrentTheme(): void {\n this.assetManifest.reload();\n let changed = false;\n\n for (const [key, link] of this.assetManifest.entries()) {\n if (this.themeRoot.ignore.ignore(key)) continue;\n if (this.themeRoot.file(key).exists) continue;\n\n const resource = this.remoteResources.get(key);\n if (!isManagedAssetResource(resource)) {\n throw new Error(`Managed asset is missing from target theme: ${key}`);\n }\n\n const nextLink: ThemeAssetLink = { ...link, sourceThemeId: this.themeId };\n delete nextLink.pending;\n delete nextLink.checksum;\n Object.assign(nextLink, resourceLink(resource));\n this.assetManifest.set(key, nextLink);\n changed = true;\n }\n\n if (changed) this.assetManifest.write();\n }\n\n // ─── Upload ───────────────────────────────────────────────────────────────\n\n /**\n * Uploads one file. Resolves with the exact text content that was sent to\n * the server (null for binary files) so callers can run diagnostics against\n * the same bytes instead of re-reading a file that may have changed on disk\n * while the request was in flight.\n */\n async uploadFile(\n file: ThemeFile,\n baseSha?: string | null,\n opts: { pendingAsset?: boolean } = {},\n ): Promise<string | null> {\n if (file.isText) {\n const content = file.read();\n const resource = await this.putResource(\n { key: file.relativePath, content },\n baseSha,\n );\n this.setRemoteResource({\n ...resource,\n key: file.relativePath,\n content,\n checksum: resource.checksum ?? file.checksum(),\n });\n return content;\n }\n\n if (isNestedBinaryThemeAsset(file)) {\n throw new Error(\n `Binary assets must be directly inside assets/: ${file.relativePath}`,\n );\n }\n\n const resource = await this.uploadBinaryFile(file, baseSha);\n this.setRemoteResource(resource);\n if (isThemeAssetKey(file.relativePath)) {\n this.externalizeBinaryFile(file, resource, opts.pendingAsset);\n }\n return null;\n }\n\n /**\n * Wraps the generated `updateThemeResource` client with the two\n * merge-aware Phase 003 additions: sending `base_sha` on the request\n * and reading the server's fresh `content_version_sha` off the\n * response so the caller can thread it forward on the next PUT.\n *\n * Accepts any `application_theme_resource` shape — text uploads pass\n * `{ key, content }`; binary uploads (after DAM + ImageKit\n * orchestration) pass `{ key, dam_asset: { ... } }`. Both must route\n * through here so `lastKnownRemoteSha` stays in lockstep with every\n * write the server has ack'd, mixed text/binary pushes included.\n *\n * The typed client hasn't been regenerated against the new OpenAPI\n * spec yet, so `base_sha` is threaded through as an extra property\n * (server accepts unknown fields on this endpoint) and the response\n * is cast to read the extra `content_version_sha`. Regeneration is\n * a follow-up; that PR will drop these casts.\n *\n * `PushConflictError` is thrown on a 409 so callers can distinguish\n * \"server rejected because of stale base\" from generic upload\n * failures.\n */\n private async putResource(\n resource: Record<string, unknown>,\n baseSha: string | null | undefined,\n ): Promise<RemoteResource> {\n const body: Record<string, unknown> = {\n application_theme_resource: resource,\n };\n if (baseSha) body[\"base_sha\"] = baseSha;\n\n try {\n const response = (await themes.updateThemeResource(\n this.api,\n this.themeId,\n body as never,\n )) as unknown as {\n application_theme_resource?: RemoteResource;\n content_version_sha?: string;\n };\n if (response.content_version_sha) {\n this.lastKnownRemoteSha = response.content_version_sha;\n }\n return (\n response.application_theme_resource ?? {\n key: typeof resource[\"key\"] === \"string\" ? resource[\"key\"] : \"\",\n checksum: null,\n }\n );\n } catch (e) {\n throw this.rethrowIfConflict(e);\n }\n }\n\n /**\n * Server-side push preflight (Phase 003a). Runs once at the start of\n * a push loop. On 200 the server's fresh `remote_sha` is stashed on\n * the syncer so subsequent per-file PUTs can carry it. On 409 a\n * `PushConflictError` is thrown carrying the server's `remote_sha`\n * from the `meta` payload — the caller renders \"pull first\".\n *\n * Skipped when `baseSha` is null/undefined so old-behavior pushes\n * (no stored `baseSha` in `.fluid-theme.json`) and `--force` pushes\n * short-circuit past the check.\n */\n /**\n * Tell Fluid the push is finished, so it commits the theme's current state\n * as one version.\n *\n * The other half of `preflightPush`. That one runs once before the file\n * loop to reject a stale base; this runs once after it, and is the only\n * thing that turns a push into a commit — the per-file writes just mark the\n * theme changed.\n *\n * It exists because the server cannot see where an operation ends. A push\n * arrives as a hundred-odd independent requests, and every way of inferring\n * \"these belong together\" either merges two publishes that happened to land\n * close together or splits one push across several commits. The client\n * knows; this says so.\n *\n * Best-effort in that it never throws: the files are already on the server\n * by the time this runs, and Fluid sweeps anything left unsynced, so a\n * failure delays the commit rather than losing it and must not fail the\n * push.\n *\n * Returns whether Fluid took the request, because a caller tracking edit\n * boundaries needs to know. A failed ask leaves the previous edit\n * uncommitted, and uploading the next one over it merges the two into\n * whichever commit eventually lands.\n */\n async requestSync(): Promise<boolean> {\n try {\n const response = (await this.api.post(\n `/api/application_themes/${this.themeId}/resources/sync`,\n {},\n )) as { content_version_sha?: string };\n if (response.content_version_sha) {\n this.lastKnownRemoteSha = response.content_version_sha;\n }\n return true;\n } catch (e) {\n console.warn(\n ` ⚠ couldn't ask Fluid to commit this push (${formatError(e)}). It will be picked up automatically.`,\n );\n return false;\n }\n }\n\n async preflightPush(baseSha: string | null | undefined): Promise<void> {\n if (!baseSha) return;\n\n try {\n const response = (await this.api.post(\n `/api/application_themes/${this.themeId}/resources/check_push`,\n { base_sha: baseSha },\n )) as { remote_sha?: string };\n if (response.remote_sha) this.lastKnownRemoteSha = response.remote_sha;\n } catch (e) {\n throw this.rethrowIfConflict(e);\n }\n }\n\n private rethrowIfConflict(e: unknown): unknown {\n const status =\n (e as { status?: number; response?: { status?: number } })?.status ??\n (e as { response?: { status?: number } })?.response?.status;\n if (status !== 409) return e;\n\n // `ApiError` from api-client-core stores the parsed response body\n // under `.data` (not `.body`). Reading the wrong key silently\n // dropped `remote_sha` in production so the \"server is at X\"\n // hint in the CLI output went missing.\n const meta = (e as { data?: { meta?: { remote_sha?: string } } })?.data\n ?.meta;\n return new PushConflictError(meta?.remote_sha ?? null);\n }\n\n private async uploadBinaryFile(\n file: ThemeFile,\n baseSha: string | null | undefined,\n ): Promise<UploadedBinaryResource> {\n // Step 1: Create DAM placeholder\n const placeholderBody = await this.api.post<{\n asset: { id: number; canonical_path: string };\n }>(\"/api/dam/assets\", {\n placeholder_asset: {\n description: `Uploaded via Fluid CLI: ${file.name}`,\n mime_type: file.mime.name,\n name: file.name,\n },\n });\n const asset = placeholderBody.asset;\n\n // Step 2: Get ImageKit auth token\n const authBody = await this.api.post<{\n token: string;\n signature: string;\n expire: number;\n }>(\"/api/dam/assets/imagekit_auth\", {});\n\n // Step 3: Upload to ImageKit via multipart\n const folder = this.canonicalPathToImageKitFolder(asset.canonical_path);\n const formData = new FormData();\n const blob = new Blob([file.readBinary() as unknown as ArrayBuffer], {\n type: file.mime.name,\n });\n formData.append(\"file\", blob, file.name);\n formData.append(\"token\", authBody.token);\n formData.append(\"signature\", authBody.signature);\n formData.append(\"expire\", String(authBody.expire));\n formData.append(\"folder\", folder);\n formData.append(\"fileName\", file.name);\n formData.append(\"publicKey\", \"public_j7s4Ih9ETh/OCp41mVQH7tlXBdU=\");\n\n const ikResp = await fetch(\n \"https://upload.imagekit.io/api/v1/files/upload\",\n {\n method: \"POST\",\n body: formData,\n },\n );\n if (!ikResp.ok) throw new Error(`ImageKit upload failed: ${ikResp.status}`);\n const ikBody = (await ikResp.json()) as {\n fileId: string;\n url: string;\n thumbnailUrl: string;\n size: number;\n height?: number;\n width?: number;\n };\n\n // Step 4: Backfill DAM asset\n const backfillPayload: Record<string, unknown> = {\n asset: {\n id: asset.id,\n imagekit_file_id: ikBody.fileId,\n imagekit_url: ikBody.url,\n mime_type: file.mime.name,\n name: file.name,\n file_size: ikBody.size,\n expected_path: asset.canonical_path,\n },\n };\n if (ikBody.height)\n (backfillPayload[\"asset\"] as Record<string, unknown>)[\"height\"] =\n ikBody.height;\n if (ikBody.width)\n (backfillPayload[\"asset\"] as Record<string, unknown>)[\"width\"] =\n ikBody.width;\n\n const backfillBody = await this.api.post<{\n asset: { code: string; default_variant_url: string };\n }>(\"/api/dam/assets/backfill_imagekit\", backfillPayload);\n\n // Step 5: Associate with theme resource. Route through `putResource`\n // so `base_sha` gets sent and `lastKnownRemoteSha` advances just\n // like text-file writes — a mixed text+binary push must keep them\n // in a single monotonic chain.\n const update = await this.putResource(\n {\n key: file.relativePath,\n dam_asset: {\n dam_asset_code: backfillBody.asset.code,\n content_type: file.mime.name,\n content_size: ikBody.size,\n filename: file.name,\n handle: backfillBody.asset.code,\n url: backfillBody.asset.default_variant_url,\n preview_image_url: ikBody.thumbnailUrl,\n },\n },\n baseSha,\n );\n\n // Older API responses can omit URL fields. Preserve the known DAM URL so\n // the manifest stays usable after a successful remote write.\n return {\n ...update,\n key: update.key || file.relativePath,\n url: update.url ?? backfillBody.asset.default_variant_url,\n damAssetCode: backfillBody.asset.code,\n assetMetadata: {\n url: backfillBody.asset.default_variant_url,\n contentType: file.mime.name,\n contentSize: ikBody.size,\n previewImageUrl: ikBody.thumbnailUrl,\n altText: file.name,\n handle: backfillBody.asset.code,\n },\n };\n }\n\n private externalizeBinaryFile(\n file: ThemeFile,\n resource: UploadedBinaryResource,\n pending: boolean | undefined,\n ): void {\n // Persist before unlinking. If persistence fails, retain the local bytes so\n // the developer can retry instead of losing the only provenance record.\n this.assetManifest.reload();\n this.assetManifest.set(file.relativePath, {\n sourceThemeId: this.themeId,\n ...resourceLink(resource),\n ...uploadedAssetMetadata(file, resource),\n ...(pending ? { pending: true } : {}),\n ...(typeof resource.damAssetCode === \"string\"\n ? { damAssetCode: resource.damAssetCode }\n : {}),\n });\n this.assetManifest.write();\n unlinkSync(file.absolutePath);\n }\n\n private canonicalPathToImageKitFolder(canonicalPath: string): string {\n const parts = canonicalPath.split(\".\");\n const companyId = parts[0] ?? \"unknown\";\n const category = parts[1] ?? \"files\";\n const assetCode = parts[2] ?? \"unknown\";\n const folderMap: Record<string, string> = {\n images: \"images\",\n videos: \"videos\",\n audio: \"audio\",\n documents: \"documents\",\n files: \"files\",\n };\n return `${companyId}/${folderMap[category] ?? \"files\"}/${assetCode}`;\n }\n\n // ─── Delete ───────────────────────────────────────────────────────────────\n\n async deleteRemoteFile(\n relativePath: string,\n baseSha?: string | null,\n ): Promise<void> {\n // Chokidar observes the intentional unlink after a binary has been\n // externalized. That unlink must never delete the server-side DAM link.\n this.assetManifest.reload();\n if (this.assetManifest.has(relativePath)) return;\n\n const body: Record<string, unknown> = {\n application_theme_resource: { key: relativePath },\n };\n if (baseSha) body[\"base_sha\"] = baseSha;\n\n try {\n const response = (await themes.deleteThemeResource(\n this.api,\n this.themeId,\n body as never,\n )) as unknown as { content_version_sha?: string };\n if (response.content_version_sha) {\n this.lastKnownRemoteSha = response.content_version_sha;\n }\n } catch (e) {\n if (!isNotFoundError(e)) throw this.rethrowIfConflict(e);\n }\n this.removeRemoteResource(relativePath);\n }\n\n // ─── Download ─────────────────────────────────────────────────────────────\n\n async downloadAll(): Promise<RemoteResource[]> {\n // Same cast rationale as `fetchChecksums`. The additional\n // `content_version_sha` field is stashed on the syncer so callers\n // (pull command) can persist it as the new `baseSha`.\n const body = (await themes.listThemeResources(\n this.api,\n this.themeId,\n )) as unknown as {\n application_theme_resources?: RemoteResource[];\n content_version_sha?: string;\n };\n const resources = body.application_theme_resources ?? [];\n this.updateChecksums(resources);\n this.lastKnownRemoteSha = body.content_version_sha ?? null;\n this.remoteResourcesLoaded = true;\n return resources;\n }\n\n async downloadBinaryAsset(url: string): Promise<Buffer> {\n const resp = await fetch(url);\n if (!resp.ok) throw new Error(`Failed to download asset: ${resp.status}`);\n return Buffer.from(await resp.arrayBuffer());\n }\n\n /**\n * Move directly-addressable binary `assets/*` resources into the local\n * manifest before merge-pull sees them. This prevents a byte download and\n * keeps those paths out of the shadow repository; their canonical state is\n * the ImageKit URL, not a local file.\n */\n async externalizePulledAssets(\n resources: RemoteResource[],\n opts: { delete: boolean; backgroundGuard?: BackgroundPullGuard },\n ): Promise<{ managedKeys: Set<string>; linked: number; errors: string[] }> {\n this.assetManifest.reload();\n\n const managedKeys = new Set<string>();\n const changedManifestKeys = new Set<string>();\n const remoteManagedKeys = new Set<string>();\n const preservedManifestKeys = new Set<string>();\n const filesToRemove = new Map<string, ThemeFile>();\n const errors: string[] = [];\n let manifestChanged = false;\n\n // ApplicationThemeResource deliberately exposes only the URL/checksum.\n // Capture FileResource metadata during the pull too, so a later push can\n // recreate the reference even if this source/development theme has since\n // been deleted. This is one small JSON request, never a binary download.\n const resourcesNeedingMetadata = resources.filter((resource) => {\n const key = resource.key;\n if (!key) return false;\n\n const file = this.themeRoot.file(key);\n if (!this.isSafeThemeFile(key, file)) return false;\n if (!isLinkableBinaryResource(resource, key, file)) return false;\n\n const existing = this.assetManifest.get(key);\n if (this.themeRoot.ignore.ignore(key) || existing?.pending) return false;\n return (\n !existing ||\n existing.url !== resource.url ||\n !assetMetadataFromLink(existing)\n );\n });\n let assetMetadata = new Map<string, RemoteAssetMetadata>();\n const unresolvedMetadataKeys = new Set<string>();\n if (resourcesNeedingMetadata.length > 0) {\n try {\n assetMetadata = await this.fetchThemeAssetMetadata(this.themeId);\n } catch (error) {\n if (error instanceof BackgroundPullChangedError) throw error;\n errors.push(`Read remote asset metadata: ${formatError(error)}`);\n for (const resource of resourcesNeedingMetadata) {\n if (resource.key) unresolvedMetadataKeys.add(resource.key);\n }\n }\n\n if (unresolvedMetadataKeys.size === 0) {\n for (const resource of resourcesNeedingMetadata) {\n const key = resource.key;\n if (!key || assetMetadata.has(assetFilename(key))) continue;\n unresolvedMetadataKeys.add(key);\n errors.push(\n `Could not find usable metadata for ${key} in theme #${this.themeId}`,\n );\n }\n }\n }\n\n opts.backgroundGuard?.assertUnchanged();\n for (const resource of resources) {\n const key = resource.key;\n if (!key) continue;\n\n const file = this.themeRoot.file(key);\n if (!this.isSafeThemeFile(key, file)) continue;\n if (!isLinkableBinaryResource(resource, key, file)) continue;\n\n remoteManagedKeys.add(key);\n if (this.themeRoot.ignore.ignore(key)) {\n if (this.assetManifest.has(key)) preservedManifestKeys.add(key);\n managedKeys.add(key);\n continue;\n }\n\n // A newly added dev asset has not necessarily been promoted to the\n // pulled source yet. It remains authoritative until a push makes it\n // durable on the selected target.\n if (this.assetManifest.get(key)?.pending) {\n preservedManifestKeys.add(key);\n managedKeys.add(key);\n continue;\n }\n\n // Do not create an incomplete URL-only reference: a later push may need\n // content type and size after this source theme no longer exists. Let\n // merge-pull retain/download just this binary instead.\n if (unresolvedMetadataKeys.has(key)) {\n // The fallback bytes are now the only trustworthy copy. A manifest\n // entry pointing at a URL the remote no longer serves is stale —\n // keeping it would let linkManagedAssets restore the older asset if\n // the local file later disappears.\n const stale = this.assetManifest.get(key);\n if (stale && stale.url !== resource.url) {\n this.assetManifest.delete(key);\n manifestChanged = true;\n }\n continue;\n }\n\n try {\n const existing = this.assetManifest.get(key);\n const metadata = assetMetadata.get(assetFilename(key));\n this.assetManifest.set(key, {\n ...existing,\n sourceThemeId: this.themeId,\n ...metadata,\n ...resourceLink(resource),\n });\n manifestChanged = true;\n changedManifestKeys.add(key);\n managedKeys.add(key);\n filesToRemove.set(key, file);\n } catch (error) {\n if (error instanceof BackgroundPullChangedError) throw error;\n errors.push(`Externalize ${key}: ${formatError(error)}`);\n }\n }\n\n if (opts.delete) {\n for (const [key, link] of this.assetManifest.entries()) {\n if (\n remoteManagedKeys.has(key) ||\n preservedManifestKeys.has(key) ||\n this.themeRoot.ignore.ignore(key) ||\n link.pending\n ) {\n continue;\n }\n this.assetManifest.delete(key);\n manifestChanged = true;\n }\n }\n\n if (manifestChanged) {\n try {\n // Persist all links first. If this fails, preserve the local bytes and\n // let merge-pull materialize the affected resources normally.\n if (opts.backgroundGuard)\n opts.backgroundGuard.mutate([\".fluid-assets.json\"], () =>\n this.assetManifest.write(),\n );\n else this.assetManifest.write();\n } catch (error) {\n if (error instanceof BackgroundPullChangedError) throw error;\n errors.push(`Persist remote asset manifest: ${formatError(error)}`);\n for (const key of changedManifestKeys) managedKeys.delete(key);\n return { managedKeys, linked: 0, errors };\n }\n }\n\n let linked = 0;\n for (const [key, file] of filesToRemove) {\n try {\n if (file.exists) {\n if (opts.backgroundGuard)\n opts.backgroundGuard.mutate([key], () =>\n unlinkSync(file.absolutePath),\n );\n else unlinkSync(file.absolutePath);\n }\n linked++;\n } catch (error) {\n if (error instanceof BackgroundPullChangedError) throw error;\n errors.push(`Externalize ${key}: ${formatError(error)}`);\n // The manifest link is safely on disk; retain the resource outside the\n // shadow even if an operating-system lock delays local cleanup.\n }\n }\n\n return { managedKeys, linked, errors };\n }\n\n private isSafeThemeFile(key: string, file: ThemeFile): boolean {\n return (\n !key.includes(\"\\0\") &&\n !key.split(/[\\\\/]/).includes(\"..\") &&\n (file.absolutePath === this.themeRoot.root ||\n file.absolutePath.startsWith(this.themeRoot.root + sep))\n );\n }\n\n private managedAssetNeedsRefresh(\n resource: RemoteResource,\n link: ThemeAssetLink,\n ): boolean {\n if (!isManagedAssetResource(resource)) return true;\n if (link.url !== undefined) return resource.url !== link.url;\n if (link.checksum !== undefined) return resource.checksum !== link.checksum;\n return true;\n }\n\n private ensureManagedAssetsAreResolved(): void {\n const unresolved = this.assetManifest\n .entries()\n .filter(\n ([key]) =>\n !this.themeRoot.ignore.ignore(key) &&\n !this.themeRoot.file(key).exists,\n )\n .map(([key]) => key)\n .filter((key) => !isManagedAssetResource(this.remoteResources.get(key)));\n if (unresolved.length > 0) {\n throw new Error(\n `Managed asset(s) could not be linked: ${unresolved.join(\", \")}`,\n );\n }\n }\n\n // ─── Full Upload ──────────────────────────────────────────────────────────\n\n async uploadTheme(\n opts: {\n delete?: boolean;\n validate?: boolean;\n linkManagedAssets?: { replace?: boolean };\n pendingBinaryAssets?: boolean;\n onProgress?: (done: number, total: number) => void;\n // The SHA `.fluid-theme.json` stored on the last pull. Sent to the\n // server on every PUT/DELETE so a stale local aborts the push\n // instead of clobbering. Null / undefined skips the check entirely\n // (force-push and pre-Phase-003 servers).\n baseSha?: string | null;\n /** The caller already ran the stale-base preflight. */\n skipPreflight?: boolean;\n /** Last acknowledged dev resource index, validated by `baseSha`. */\n remoteState?: DevRemoteState;\n } = {},\n ): Promise<SyncResult> {\n const localFiles = this.themeRoot.files();\n assertNoCaseCollisions(localFiles.map((file) => file.relativePath));\n const result: SyncResult = {\n uploaded: 0,\n deleted: 0,\n downloaded: 0,\n linked: 0,\n errors: [],\n validationFailed: false,\n };\n\n // Schema validation pass\n if (opts.validate) {\n for (const file of localFiles) {\n if (!file.isLiquid) continue;\n const diagnostics = file.validateSchema();\n const errors = diagnostics.filter((d) => d.severity === \"error\");\n for (const d of errors) {\n result.errors.push(`${file.relativePath}: ${d.message}`);\n }\n }\n if (result.errors.length > 0) {\n result.validationFailed = true;\n return result;\n }\n }\n\n if (opts.remoteState) {\n this.useDevRemoteState(opts.remoteState);\n } else {\n await this.fetchChecksums();\n }\n\n // Preflight before any state change so a stale local aborts with\n // zero writes. On 409, `PushConflictError` propagates to the CLI\n // and the per-file loop below never runs.\n if (!opts.skipPreflight) await this.preflightPush(opts.baseSha);\n\n // Roll the base_sha forward as the server bumps its version on\n // each successful write. Start from the caller's stored SHA (from\n // pull) and update from every response — the next PUT carries the\n // freshest SHA the server has given us so a concurrent third-party\n // write mid-loop 409s cleanly instead of the CLI overwriting.\n let baseSha = opts.baseSha ?? null;\n\n if (opts.linkManagedAssets) {\n result.linked = await this.linkManagedAssets(opts.linkManagedAssets);\n // FileResource writes can change the server's content version. The list\n // after reference creation gives the next normal resource write a fresh\n // base when the server supports Phase 003a.\n baseSha = this.lastKnownRemoteSha ?? baseSha;\n } else if (this.assetManifestInstance) {\n this.assetManifest.reload();\n this.ensureManagedAssetsAreResolved();\n }\n\n const toUpload = localFiles.filter((f) => f.exists && this.hasChanged(f));\n let done = 0;\n for (const file of toUpload) {\n try {\n await this.uploadFile(file, baseSha, {\n pendingAsset: opts.pendingBinaryAssets,\n });\n // `putResource` updates `lastKnownRemoteSha` from the server's\n // response. Thread it forward so the next iteration carries\n // the current SHA — this is how mid-push races surface.\n baseSha = this.lastKnownRemoteSha;\n result.uploaded++;\n } catch (e) {\n if (e instanceof PushConflictError) throw e;\n result.errors.push(`Upload ${file.relativePath}: ${formatError(e)}`);\n }\n opts.onProgress?.(++done, toUpload.length);\n }\n\n if (opts.delete) {\n const localPaths = new Set(localFiles.map((f) => f.relativePath));\n for (const key of this.assetManifest.keys()) localPaths.add(key);\n const toDelete = this.remoteKeys().filter(\n (key) =>\n this.canDeleteRemoteResource(key) &&\n !localPaths.has(key) &&\n !this.themeRoot.ignore.ignore(key),\n );\n for (const key of toDelete) {\n try {\n await this.deleteRemoteFile(key, baseSha);\n baseSha = this.lastKnownRemoteSha;\n result.deleted++;\n } catch (e) {\n if (e instanceof PushConflictError) throw e;\n result.errors.push(`Delete ${key}: ${formatError(e)}`);\n }\n }\n }\n\n return result;\n }\n\n // ─── Full Download ────────────────────────────────────────────────────────\n\n async downloadTheme(\n opts: {\n delete?: boolean;\n skip?: Set<string>;\n onProgress?: (done: number, total: number) => void;\n } = {},\n ): Promise<SyncResult & { skipped: number }> {\n const resources = await this.downloadAll();\n const externalizedAssets = await this.externalizePulledAssets(resources, {\n delete: opts.delete ?? false,\n });\n const result: SyncResult & { skipped: number } = {\n uploaded: 0,\n deleted: 0,\n downloaded: 0,\n linked: externalizedAssets.linked,\n skipped: 0,\n errors: [...externalizedAssets.errors],\n validationFailed: false,\n };\n\n let done = 0;\n for (const resource of resources) {\n if (externalizedAssets.managedKeys.has(resource.key)) {\n opts.onProgress?.(++done, resources.length);\n continue;\n }\n if (opts.skip?.has(resource.key)) {\n result.skipped++;\n opts.onProgress?.(++done, resources.length);\n continue;\n }\n\n const file = this.themeRoot.file(resource.key);\n\n // Guard against path traversal from malicious API responses\n if (!this.isSafeThemeFile(resource.key, file)) {\n result.errors.push(`Download ${resource.key}: path traversal detected`);\n opts.onProgress?.(++done, resources.length);\n continue;\n }\n\n try {\n if (resource.resource_type === \"FileResource\" && resource.url) {\n const buf = await this.downloadBinaryAsset(resource.url);\n file.write(buf);\n } else if (\n resource.content !== undefined &&\n resource.content !== null\n ) {\n const content =\n typeof resource.content === \"string\"\n ? resource.content\n : JSON.stringify(resource.content);\n file.write(content);\n }\n result.downloaded++;\n } catch (e) {\n result.errors.push(`Download ${resource.key}: ${formatError(e)}`);\n }\n opts.onProgress?.(++done, resources.length);\n }\n\n if (opts.delete) {\n const remoteKeys = new Set(resources.map((r) => r.key));\n for (const file of this.themeRoot.files()) {\n if (remoteKeys.has(file.relativePath)) continue;\n // Preserve local stylesheets that the backend hides under\n // STYLESHEET_STRICT_INPUT — their absence from the resources\n // index is intentional (theme-level styles.css /\n // global_styles.css and per-template composite\n // {type}/{name}/styles.css), not a signal that the merchant\n // deleted them.\n if (isStylesheetKey(file.relativePath)) continue;\n\n try {\n unlinkSync(file.absolutePath);\n result.deleted++;\n } catch {\n // ignore\n }\n }\n }\n\n return result;\n }\n}\n\nfunction isLinkableBinaryResource(\n resource: RemoteResource,\n key: string,\n file: ThemeFile,\n): boolean {\n return (\n isThemeAssetKey(key) && isManagedAssetResource(resource) && !file.isText\n );\n}\n\nfunction isNestedBinaryThemeAsset(file: ThemeFile): boolean {\n return (\n !file.isText &&\n file.relativePath.startsWith(\"assets/\") &&\n !isThemeAssetKey(file.relativePath)\n );\n}\n\nfunction isManagedAssetResource(\n resource: RemoteResource | undefined,\n): resource is RemoteResource {\n return (\n resource?.resource_type === \"FileResource\" &&\n typeof resource.url === \"string\" &&\n resource.url.length > 0\n );\n}\n\nfunction remoteResourceFromState(state: RemoteResourceState): RemoteResource {\n return {\n key: state.key,\n checksum: state.checksum,\n content: state.contentPresent ? \"\" : null,\n resource_type: state.resourceType,\n resource_id: state.resourceId,\n url: state.url,\n };\n}\n\nfunction remoteResourceState(resource: RemoteResource): RemoteResourceState {\n return {\n key: resource.key,\n checksum: resource.checksum,\n contentPresent: resource.content != null,\n resourceType: resource.resource_type,\n resourceId: resource.resource_id,\n url: resource.url,\n };\n}\n\nfunction isNotFoundError(error: unknown): boolean {\n return isApiError(error) && error.status === 404;\n}\n\nfunction resourceLink(\n resource: RemoteResource,\n): Pick<ThemeAssetLink, \"checksum\" | \"url\"> {\n return {\n ...(typeof resource.checksum === \"string\" && resource.checksum.length > 0\n ? { checksum: resource.checksum }\n : {}),\n ...(typeof resource.url === \"string\" && resource.url.length > 0\n ? { url: resource.url }\n : {}),\n };\n}\n\nfunction assetFilename(key: string): string {\n return key.slice(\"assets/\".length);\n}\n\nfunction assetMetadataFromLink(\n link: ThemeAssetLink,\n): RemoteAssetMetadata | undefined {\n if (\n typeof link.url !== \"string\" ||\n link.url.length === 0 ||\n typeof link.contentType !== \"string\" ||\n link.contentType.length === 0 ||\n typeof link.contentSize !== \"number\" ||\n !Number.isInteger(link.contentSize) ||\n link.contentSize <= 0\n ) {\n return undefined;\n }\n\n return {\n url: link.url,\n contentType: link.contentType,\n contentSize: link.contentSize,\n ...(typeof link.previewImageUrl === \"string\"\n ? { previewImageUrl: link.previewImageUrl }\n : {}),\n ...(typeof link.altText === \"string\" ? { altText: link.altText } : {}),\n ...(typeof link.handle === \"string\" ? { handle: link.handle } : {}),\n };\n}\n\nfunction uploadedAssetMetadata(\n file: ThemeFile,\n resource: UploadedBinaryResource,\n): RemoteAssetMetadata {\n if (resource.assetMetadata) return resource.assetMetadata;\n if (!resource.url) {\n throw new Error(`Uploaded asset has no URL: ${file.relativePath}`);\n }\n\n return {\n url: resource.url,\n contentType: file.mime.name,\n contentSize: file.size(),\n altText: file.name,\n ...(typeof resource.damAssetCode === \"string\"\n ? { handle: resource.damAssetCode }\n : {}),\n };\n}\n\nfunction parseThemeAssetMetadata(\n value: unknown,\n): { filename: string; metadata: RemoteAssetMetadata } | undefined {\n if (!isRecord(value)) return undefined;\n\n const filename = nonEmptyString(value[\"filename\"]);\n const url = nonEmptyString(value[\"url\"]);\n const contentType = nonEmptyString(value[\"content_type\"]);\n const contentSize = positiveInteger(value[\"content_size\"]);\n if (!filename || !url || !contentType || !contentSize) return undefined;\n\n const previewImageUrl = nonEmptyString(value[\"preview_image_url\"]);\n const altText = optionalString(value[\"alt_text\"]);\n const handle = nonEmptyString(value[\"handle\"]);\n return {\n filename,\n metadata: {\n url,\n contentType,\n contentSize,\n ...(previewImageUrl ? { previewImageUrl } : {}),\n ...(altText !== undefined ? { altText } : {}),\n ...(handle ? { handle } : {}),\n },\n };\n}\n\nfunction createdFileResourceId(value: unknown): number | undefined {\n if (!isRecord(value) || !isRecord(value[\"file_resource\"])) {\n return undefined;\n }\n return positiveInteger(value[\"file_resource\"][\"id\"]);\n}\n\nfunction positiveInteger(value: unknown): number | undefined {\n if (typeof value === \"number\" && Number.isInteger(value) && value > 0) {\n return value;\n }\n if (typeof value !== \"string\" || !/^\\d+$/.test(value)) return undefined;\n\n const parsed = Number(value);\n return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n\nfunction nonEmptyString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\nfunction optionalString(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n","/**\n * Heuristic-only check for obviously unbalanced liquid delimiters\n * (`{% %}` and `{{ }}`). This is NOT a liquid parser — it only tracks\n * opening delimiters until they are closed. It exists to give watch-mode users a signal\n * when a save is liquid-syntax-broken: the server accepts\n * syntax-broken liquid silently on upload, and the storefront\n * renderer then serves stale content for that section with no error\n * anywhere else in the pipeline.\n *\n * Closing-looking tokens without a preceding Liquid opener are intentionally\n * ignored. Liquid files commonly contain CSS such as `width:100%}` or adjacent\n * block braces (`}}`), so treating every close token as Liquid creates noisy\n * false warnings on valid theme files.\n *\n * Known false positive: delimiters written literally inside a\n * `{% raw %}...{% endraw %}` block are still counted and can trip\n * this check even though the liquid is valid. Acceptable for a\n * warn-only heuristic — a real fix requires parsing liquid, which is\n * out of scope here (see the server-side validation note in the PR).\n */\nexport function hasUnbalancedLiquidDelimiters(content: string): boolean {\n let unclosedTags = 0;\n let unclosedOutputs = 0;\n\n for (const token of content.matchAll(/\\{%|%\\}|\\{\\{|\\}\\}/g)) {\n switch (token[0]) {\n case \"{%\":\n unclosedTags += 1;\n break;\n case \"%}\":\n if (unclosedTags > 0) unclosedTags -= 1;\n break;\n case \"{{\":\n unclosedOutputs += 1;\n break;\n case \"}}\":\n if (unclosedOutputs > 0) unclosedOutputs -= 1;\n break;\n }\n }\n\n return unclosedTags > 0 || unclosedOutputs > 0;\n}\n\nexport interface LiquidBlockTagDiagnostic {\n severity: \"error\";\n message: string;\n}\n\nconst BLOCK_TAGS = new Map([\n [\"capture\", \"endcapture\"],\n [\"case\", \"endcase\"],\n [\"comment\", \"endcomment\"],\n [\"for\", \"endfor\"],\n [\"form\", \"endform\"],\n [\"if\", \"endif\"],\n [\"ifchanged\", \"endifchanged\"],\n [\"javascript\", \"endjavascript\"],\n [\"paginate\", \"endpaginate\"],\n [\"raw\", \"endraw\"],\n [\"schema\", \"endschema\"],\n [\"style\", \"endstyle\"],\n [\"stylesheet\", \"endstylesheet\"],\n [\"tablerow\", \"endtablerow\"],\n [\"unless\", \"endunless\"],\n]);\n\nconst CLOSING_TAGS = new Set(BLOCK_TAGS.values());\nconst OPAQUE_BLOCK_TAGS = new Set([\n \"comment\",\n \"javascript\",\n \"raw\",\n \"schema\",\n \"style\",\n \"stylesheet\",\n]);\n\ninterface OpenBlock {\n name: string;\n expectedClose: string;\n line: number;\n}\n\n/**\n * Find structurally unbalanced Liquid block tags such as an `{% if %}` with\n * no `{% endif %}`. This intentionally recognizes only established paired\n * tags; custom and inline tags are ignored rather than guessed at.\n *\n * Content inside raw/comment/schema/style/javascript blocks is opaque to\n * Liquid and therefore skipped until that block's matching close tag. This\n * prevents CSS, JSON, and examples containing Liquid-looking text from\n * producing false errors.\n */\nexport function findLiquidBlockTagDiagnostics(\n content: string,\n): LiquidBlockTagDiagnostic[] {\n const stack: OpenBlock[] = [];\n const diagnostics: LiquidBlockTagDiagnostic[] = [];\n let line = 1;\n let previousTagIndex = 0;\n\n const processTag = (name: string, tagLine: number): void => {\n const open = stack.at(-1);\n\n if (open && OPAQUE_BLOCK_TAGS.has(open.name)) {\n if (name === open.expectedClose) stack.pop();\n return;\n }\n\n const expectedClose = BLOCK_TAGS.get(name);\n if (expectedClose) {\n stack.push({ name, expectedClose, line: tagLine });\n return;\n }\n\n if (!CLOSING_TAGS.has(name)) return;\n\n if (!open) {\n diagnostics.push({\n severity: \"error\",\n message: `Unexpected Liquid tag '{% ${name} %}' on line ${tagLine}; there is no open block to close.`,\n });\n return;\n }\n\n if (name !== open.expectedClose) {\n diagnostics.push({\n severity: \"error\",\n message: `Mismatched Liquid tag '{% ${name} %}' on line ${tagLine}; '{% ${open.name} %}' from line ${open.line} must close with '{% ${open.expectedClose} %}'.`,\n });\n return;\n }\n\n stack.pop();\n };\n\n for (const match of content.matchAll(\n /\\{%-?\\s*([a-zA-Z_][\\w-]*)\\b(?:(?!\\{%)[\\s\\S])*?-?%\\}/g,\n )) {\n const name = match[1]?.toLowerCase();\n if (!name) continue;\n\n const index = match.index ?? 0;\n // Count each character at most once across the scan. Re-slicing from the\n // start for every tag turns large generated templates into O(n²) work.\n for (let cursor = previousTagIndex; cursor < index; cursor++) {\n if (content.charCodeAt(cursor) === 10) line += 1;\n }\n previousTagIndex = index;\n\n const open = stack.at(-1);\n if (name === \"liquid\" && !(open && OPAQUE_BLOCK_TAGS.has(open.name))) {\n // `{% liquid %}` places one delimiter-free statement on each line. Feed\n // those statements through the same stack so this supported syntax\n // cannot bypass lint, push, or watch-mode validation.\n const statements = match[0]\n .replace(/^\\{%-?\\s*liquid\\b/i, \"\")\n .replace(/-?%\\}$/, \"\")\n .split(\"\\n\");\n for (const [offset, statement] of statements.entries()) {\n const statementName = /^\\s*([a-zA-Z_][\\w-]*)\\b/.exec(statement)?.[1];\n if (!statementName) continue;\n processTag(statementName.toLowerCase(), line + offset);\n }\n } else {\n processTag(name, line);\n }\n }\n\n for (const open of stack.reverse()) {\n diagnostics.push({\n severity: \"error\",\n message: `Unclosed Liquid tag '{% ${open.name} %}' on line ${open.line}; expected '{% ${open.expectedClose} %}'.`,\n });\n }\n\n return diagnostics;\n}\n","import net from \"node:net\";\n\n/**\n * The dev command does real work before it ever binds a port: it resolves\n * (or creates) a server-side dev theme and runs a full initial sync, which\n * can take minutes on a large theme. If the requested port is already taken\n * — most commonly by another `fluid theme dev` or the Mist Desktop preview,\n * which both default to 9292 — all of that work is wasted and the process\n * used to die with a raw `EADDRINUSE` stack trace. Call this before any of\n * that work starts so we fail fast with a clear message instead.\n */\nexport class PortInUseError extends Error {\n constructor(\n public readonly host: string,\n public readonly port: number,\n ) {\n super(formatPortConflictMessage(host, port));\n this.name = \"PortInUseError\";\n }\n}\n\nexport function formatPortConflictMessage(host: string, port: number): string {\n return (\n `Port ${port} on ${host} is already in use — likely another ` +\n \"`fluid theme dev` or the Mist Desktop preview. Stop the other \" +\n \"server or pass --port <number>.\"\n );\n}\n\n/**\n * Attempt to bind `host:port`, then immediately release it. Resolves if the\n * port is free; rejects with `PortInUseError` on `EADDRINUSE`/`EACCES`, or\n * the raw error for anything else unexpected.\n */\nexport function checkPortAvailable(host: string, port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n const server = net.createServer();\n\n server.once(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\" || err.code === \"EACCES\") {\n reject(new PortInUseError(host, port));\n } else {\n reject(err);\n }\n });\n\n server.once(\"listening\", () => {\n server.close(() => resolve());\n });\n\n server.listen(port, host);\n });\n}\n","import http from \"node:http\";\nimport { SSEStream } from \"./sse.js\";\nimport { proxyRequest } from \"./proxy.js\";\nimport { watchTheme } from \"./watcher.js\";\nimport { PushConflictError, Syncer } from \"../syncer.js\";\nimport {\n findLiquidBlockTagDiagnostics,\n hasUnbalancedLiquidDelimiters,\n} from \"../liquid-delimiters.js\";\nimport type { ThemeRoot } from \"../root.js\";\nimport type { ApiClient } from \"../../api.js\";\nimport { formatPortConflictMessage } from \"./port-preflight.js\";\nimport { assertNoCaseCollisions } from \"../case-collisions.js\";\nimport { isStylesheetKey } from \"../stylesheet-keys.js\";\nimport { ThemeAssetManifest } from \"../asset-manifest.js\";\nimport type { DevRemoteState } from \"../dev-remote-baseline.js\";\n\nfunction timestamp(): string {\n return new Date().toLocaleTimeString(\"en-US\", { hour12: false });\n}\n\nexport interface DevServerOptions {\n host: string;\n port: number;\n reloadMode: \"full-page\" | \"off\";\n /** Cached dev resource index paired with its acknowledged remote version. */\n initialSync?: DevRemoteState;\n /** Persist the complete dev resource baseline and its version atomically. */\n onRemoteState?: (state: DevRemoteState) => void;\n /** Discard persisted trust after the server rejects a watched write. */\n onRemoteStateInvalidated?: () => void;\n}\n\nexport interface DevServerTheme {\n id: number;\n name: string;\n company: string;\n editorUrl?: string;\n}\n\nexport async function startDevServer(\n api: ApiClient,\n theme: DevServerTheme,\n themeRoot: ThemeRoot,\n opts: DevServerOptions & { validate?: boolean },\n onReady?: (address: string) => void,\n): Promise<() => void> {\n const sse = new SSEStream();\n const syncer = new Syncer(api, theme.id, themeRoot);\n let remoteStateSafe = true;\n const invalidateRemoteState = () => {\n if (!remoteStateSafe) return;\n remoteStateSafe = false;\n try {\n opts.onRemoteStateInvalidated?.();\n } catch {\n // The stale version will fail preflight on the next startup.\n }\n };\n const recordRemoteState = () => {\n if (!remoteStateSafe) return;\n if (opts.onRemoteState) {\n const state = syncer.devRemoteState(\n new ThemeAssetManifest(themeRoot.root).fingerprint(),\n );\n if (state) {\n try {\n opts.onRemoteState(state);\n } catch {\n invalidateRemoteState();\n }\n }\n }\n };\n\n const pendingUpdates = new Set<string>();\n\n // ── Initial sync ─────────────────────────────────────────────────────────\n console.log(`\\nSyncing theme ${theme.name} (#${theme.id})…`);\n const progress = (done: number, total: number) => {\n process.stdout.write(`\\r Uploading ${done}/${total} files…`);\n };\n const uploadFromRemoteIndex = () =>\n syncer.uploadTheme({\n delete: true,\n validate: opts.validate,\n linkManagedAssets: { replace: true },\n pendingBinaryAssets: true,\n onProgress: progress,\n });\n let syncResult;\n if (opts.initialSync) {\n let remoteStateIsTrusted = false;\n try {\n syncer.useDevRemoteState(opts.initialSync);\n await syncer.preflightPush(opts.initialSync.remoteSha);\n remoteStateIsTrusted = true;\n } catch (error) {\n if (!(error instanceof PushConflictError)) throw error;\n }\n\n syncResult = remoteStateIsTrusted\n ? await syncer.uploadTheme({\n delete: true,\n validate: opts.validate,\n linkManagedAssets: { replace: true },\n pendingBinaryAssets: true,\n baseSha: syncer.remoteSha(),\n remoteState: opts.initialSync,\n skipPreflight: true,\n onProgress: progress,\n })\n : await uploadFromRemoteIndex();\n } else {\n syncResult = await uploadFromRemoteIndex();\n }\n process.stdout.write(\"\\n\");\n if (syncResult.linked > 0) {\n console.log(` Saved ${syncResult.linked} remote asset reference(s).`);\n }\n if (syncResult.validationFailed) {\n console.error(\n `\\nSchema validation failed (${syncResult.errors.length} error(s)). Use --force to skip.\\n`,\n );\n for (const e of syncResult.errors) console.error(` ${e}`);\n process.exit(1);\n } else if (syncResult.errors.length > 0) {\n invalidateRemoteState();\n for (const e of syncResult.errors) console.error(` ${e}`);\n if (syncResult.uploaded + syncResult.deleted === 0) process.exit(1);\n }\n if (syncResult.errors.length === 0) {\n recordRemoteState();\n }\n\n // ── File watcher ─────────────────────────────────────────────────────────\n //\n // Uploading a file no longer commits anything — Fluid only records that the\n // theme changed, and something has to ask for the commit. `theme push` asks\n // once when its loop ends; the watcher has no such moment, since chokidar\n // reports one event per file and nothing in it says whether this is a lone\n // save or the 200th write of a restore.\n //\n // So ask once the writes stop — but decide where \"the writes\" end by when\n // events ARRIVED, not by when their handlers ran. Handlers are serialized\n // behind awaited uploads, so a genuinely separate edit can queue behind a\n // slow one and start immediately after it; timing the handlers would fold\n // the two into a single commit and erase the boundary between them.\n //\n // An event that arrived within the window continues the operation already\n // in flight, so its pending ask is cancelled and rescheduled. One that\n // arrived after a real pause ends that operation, so the ask goes out NOW\n // rather than being cancelled — otherwise the earlier edit would be\n // swallowed into this one's commit.\n //\n // Asks are also chained against the uploads. A fire-and-forget request can\n // overlap the next batch, committing a half-written operation or leaving\n // the rest of it pending; awaiting the outstanding one before uploading\n // keeps each commit to a settled state.\n //\n // If the server stops with an ask still pending, Fluid's sweeper picks the\n // change up; the cost is a delay, never a lost commit.\n const SYNC_IDLE_MS = 2_000;\n let lastArrivedAt = 0;\n let pendingSync: ReturnType<typeof setTimeout> | null = null;\n let syncInFlight: Promise<void> = Promise.resolve();\n\n // Tracks an ask Fluid did not take. Until it lands the previous edit is\n // uncommitted, and uploading the next one over it merges the two into\n // whichever commit eventually arrives — so the boundary is retried before\n // any further writes go out. A retry that also fails has done what a client\n // can: the content is safe on the server and the sweeper commits it, but\n // the two edits will share a version.\n let askOwed = false;\n let remoteWritesBlocked = false;\n const blockRemoteWrites = (error: PushConflictError): void => {\n if (remoteWritesBlocked) return;\n remoteWritesBlocked = true;\n invalidateRemoteState();\n console.error(\n `\\n[Watcher] Remote theme changed outside this dev session (${error.message}). Restart theme dev to compare the current remote state before writing again.`,\n );\n };\n const sendSync = (): void => {\n syncInFlight = syncInFlight.then(async () => {\n const accepted = await syncer.requestSync();\n askOwed = !accepted;\n if (accepted) recordRemoteState();\n });\n };\n const flushSyncNow = (): void => {\n if (!pendingSync) return;\n clearTimeout(pendingSync);\n pendingSync = null;\n sendSync();\n };\n const scheduleSync = (): void => {\n if (pendingSync) clearTimeout(pendingSync);\n pendingSync = setTimeout(() => {\n pendingSync = null;\n sendSync();\n }, SYNC_IDLE_MS);\n };\n\n const stopWatcher = watchTheme(\n themeRoot,\n async (modified, added, removed, arrivedAt) => {\n if (arrivedAt - lastArrivedAt > SYNC_IDLE_MS) {\n // A real pause: whatever was pending belongs to the previous edit.\n flushSyncNow();\n } else if (pendingSync) {\n // Same operation still arriving — hold the ask.\n clearTimeout(pendingSync);\n pendingSync = null;\n }\n lastArrivedAt = arrivedAt;\n // Never upload across an outstanding ask, or it commits a half-written\n // operation.\n await syncInFlight;\n // An ask Fluid refused still owes the edit before this one its own\n // commit. Retry before writing over it.\n if (askOwed) {\n sendSync();\n await syncInFlight;\n }\n if (remoteWritesBlocked) return;\n\n try {\n assertNoCaseCollisions(\n themeRoot.files().map((file) => file.relativePath),\n );\n } catch (error) {\n console.error(`\\n[Watcher] Sync blocked: ${String(error)}`);\n return;\n }\n\n const changed = [...modified, ...added];\n let wroteRemote = false;\n\n for (const file of changed) {\n // Validate schema on liquid files during dev (warn, don't block)\n if (opts.validate && file.isLiquid) {\n const diagnostics = file.validateSchema();\n for (const d of diagnostics) {\n const prefix =\n d.severity === \"error\" ? \"Schema error\" : \"Schema warning\";\n console.warn(`\\n[${prefix}] ${file.relativePath}: ${d.message}`);\n }\n }\n\n pendingUpdates.add(file.relativePath);\n try {\n // uploadFile() resolves with the exact bytes it sent, so the\n // diagnostic below always describes the uploaded content —\n // never a re-read of a file an editor may have changed,\n // replaced, or deleted while the request was in flight.\n const uploadedContent = await syncer.uploadFile(\n file,\n syncer.remoteSha(),\n { pendingAsset: true },\n );\n wroteRemote = true;\n console.log(` ✓ synced ${file.relativePath} (${timestamp()})`);\n // \"synced\" is true of the upload and false of the page. The\n // storefront inlines a stylesheet from a Themes::FileResource that a\n // background job rewrites after the publish commits, so\n // `inline_asset_content` keeps emitting the previous bytes for a\n // moment after the server accepts this write. Say so, or the ✓ reads\n // as \"the rendered CSS is up to date\" (CURRENT-3899).\n if (isStylesheetKey(file.relativePath)) {\n console.warn(\n ` ⚠ ${file.relativePath}: the storefront inlines this CSS from a published asset, so it serves the previous bytes until the stylesheet asset sync finishes`,\n );\n }\n // Cheap warn-only heuristic: the server accepts syntax-broken\n // liquid silently, and the storefront then serves stale\n // content for the section with no error anywhere. This does\n // not parse liquid — it can false-positive on delimiters\n // written literally inside {% raw %} — but it's the only\n // client-side signal a save is likely broken.\n if (\n file.isLiquid &&\n uploadedContent !== null &&\n hasUnbalancedLiquidDelimiters(uploadedContent)\n ) {\n console.warn(\n ` ⚠ ${file.relativePath}: unbalanced liquid delimiters — the storefront may silently serve stale content for this section`,\n );\n }\n if (file.isLiquid && uploadedContent !== null) {\n for (const diagnostic of findLiquidBlockTagDiagnostics(\n uploadedContent,\n )) {\n console.warn(` ⚠ ${file.relativePath}: ${diagnostic.message}`);\n }\n }\n } catch (e) {\n if (e instanceof PushConflictError) {\n blockRemoteWrites(e);\n break;\n }\n invalidateRemoteState();\n console.error(\n `\\n[Watcher] Upload failed: ${file.relativePath}: ${e}`,\n );\n } finally {\n pendingUpdates.delete(file.relativePath);\n }\n }\n\n if (remoteWritesBlocked) return;\n\n for (const file of removed) {\n if (themeRoot.ignore.ignore(file.relativePath)) continue;\n try {\n await syncer.deleteRemoteFile(file.relativePath, syncer.remoteSha());\n wroteRemote = true;\n console.log(` ✓ removed ${file.relativePath}`);\n } catch (error) {\n if (error instanceof PushConflictError) {\n blockRemoteWrites(error);\n break;\n }\n invalidateRemoteState();\n }\n }\n\n if (remoteWritesBlocked) return;\n\n if (wroteRemote) recordRemoteState();\n\n if (removed.length > 0) {\n sse.broadcast(JSON.stringify({ reload_page: true }));\n } else if (changed.length > 0) {\n sse.broadcast(\n JSON.stringify({ modified: changed.map((f) => f.relativePath) }),\n );\n }\n\n scheduleSync();\n },\n );\n\n // ── HTTP server ───────────────────────────────────────────────────────────\n const server = http.createServer(async (req, res) => {\n if (req.url === \"/hot-reload\") {\n sse.add(res);\n return;\n }\n\n try {\n await proxyRequest(req, res, {\n company: theme.company,\n themeId: theme.id,\n reloadMode: opts.reloadMode,\n pendingFiles: () =>\n [...pendingUpdates]\n .map((p) => themeRoot.file(p))\n .filter((f) => f.isText)\n .map((f) => ({\n relativePath: f.relativePath,\n read: () => f.read(),\n })),\n });\n } catch (e) {\n console.error(`[Proxy] ${req.method} ${req.url} → ${e}`);\n if (!res.headersSent) {\n // Surface the real upstream failure instead of a bare 502.\n // Every local render round-trips through <company>.fluid.app,\n // so this error is usually environmental — a TLS-inspecting\n // security agent whose root CA the browser trusts via the OS\n // keychain but Node does not, DNS, or a dropped connection —\n // and a naked \"Bad Gateway\" sends humans and QA agents off\n // chasing the theme instead of the network. Diagnosed live:\n // four workflow page steps went needs-review over one masked\n // cert error.\n const message = e instanceof Error ? e.message : String(e);\n res.writeHead(502, { \"content-type\": \"text/plain; charset=utf-8\" });\n res.end(\n `Bad Gateway — the local preview could not reach ${theme.company}.fluid.app: ${message}\\n` +\n \"This is the dev machine's network path to Fluid, not the theme. \" +\n \"Common causes: TLS-inspecting security software (its root CA is in the OS keychain, which Node does not read — set NODE_EXTRA_CA_CERTS to its certificate), DNS, or a proxy. \" +\n \"The same error is logged by the theme dev server process.\",\n );\n }\n }\n });\n\n // The preflight check in `dev.ts` runs before the (potentially minutes-long)\n // initial sync above, but the port could still be grabbed by another\n // process in the window between that check and this `listen()` call.\n // Handle that race the same way: a friendly message and a clean exit\n // instead of an unhandled `EADDRINUSE`/`EACCES` stack trace.\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\" || err.code === \"EACCES\") {\n console.error(formatPortConflictMessage(opts.host, opts.port));\n process.exit(1);\n }\n reject(err);\n });\n server.listen(opts.port, opts.host, () => resolve());\n });\n\n const address = `http://${opts.host}:${opts.port}`;\n onReady?.(address);\n\n // ── Teardown ──────────────────────────────────────────────────────────────\n return function stop() {\n sse.close();\n stopWatcher();\n server.close();\n };\n}\n","import { createHash, randomBytes } from \"node:crypto\";\nimport {\n mkdirSync,\n readFileSync,\n renameSync,\n rmSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { assertNoCaseCollisions } from \"./case-collisions.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport type { ShadowRepo } from \"./shadow-repo.js\";\nimport {\n MANAGED_ASSET_SHADOW_SENTINEL,\n ThemeAssetManifest,\n} from \"./asset-manifest.js\";\n\nconst BASELINE_VERSION = 1;\nconst BASELINE_FILE = join(\".fluid-theme\", \"dev-baseline.json\");\n\nexport interface RemoteResourceState {\n key: string;\n checksum: string | null;\n contentPresent: boolean;\n resourceType?: string | null;\n resourceId?: number | null;\n url?: string | null;\n}\n\nexport interface DevRemoteState {\n themeId: number;\n remoteSha: string;\n assetManifestSha: string;\n resources: RemoteResourceState[];\n}\n\nexport function readDevRemoteBaseline(\n themeRoot: string,\n themeId: number,\n assetManifestSha: string,\n): DevRemoteState | null {\n try {\n const parsed = JSON.parse(\n readFileSync(join(themeRoot, BASELINE_FILE), \"utf-8\"),\n );\n const state = parseDocument(parsed);\n if (state.themeId !== themeId) return null;\n if (state.assetManifestSha !== assetManifestSha) return null;\n return state;\n } catch {\n return null;\n }\n}\n\nexport function writeDevRemoteBaseline(\n themeRoot: string,\n state: DevRemoteState,\n): void {\n const document = { version: BASELINE_VERSION, ...state };\n parseDocument(document);\n\n const path = join(themeRoot, BASELINE_FILE);\n const tempPath = `${path}.${randomBytes(6).toString(\"hex\")}.tmp`;\n try {\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(tempPath, `${JSON.stringify(document)}\\n`, {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n renameSync(tempPath, path);\n } catch (error) {\n rmSync(tempPath, { force: true });\n throw error;\n }\n}\n\nexport function removeDevRemoteBaseline(themeRoot: string): void {\n rmSync(join(themeRoot, BASELINE_FILE), { force: true });\n}\n\nexport async function devRemoteStateFromSourceShadow(\n themeRoot: ThemeRoot,\n shadow: ShadowRepo,\n themeId: number,\n remoteSha: string,\n): Promise<DevRemoteState | null> {\n try {\n if (!(await shadow.hasHead())) return null;\n\n const assetManifest = new ThemeAssetManifest(themeRoot.root);\n const resources: RemoteResourceState[] = [];\n for (const key of await shadow.headPaths()) {\n const content = await shadow.blobAtHead(key);\n if (!content) return null;\n const asset = assetManifest.get(key);\n if (asset) {\n if (\n asset.pending ||\n !asset.url ||\n !content.equals(Buffer.from(MANAGED_ASSET_SHADOW_SENTINEL))\n ) {\n return null;\n }\n resources.push({\n key,\n checksum: asset.checksum ?? null,\n contentPresent: false,\n resourceType: \"FileResource\",\n url: asset.url,\n });\n continue;\n }\n\n if (content.equals(Buffer.from(MANAGED_ASSET_SHADOW_SENTINEL)))\n return null;\n resources.push({\n key,\n checksum: createHash(\"sha256\").update(content).digest(\"hex\"),\n contentPresent: true,\n });\n }\n\n return {\n themeId,\n remoteSha,\n assetManifestSha: assetManifest.fingerprint(),\n resources,\n };\n } catch {\n return null;\n }\n}\n\nfunction parseDocument(document: unknown): DevRemoteState {\n if (!isRecord(document) || document[\"version\"] !== BASELINE_VERSION) {\n throw new Error(`expected version ${BASELINE_VERSION}`);\n }\n\n const themeId = document[\"themeId\"];\n const remoteSha = document[\"remoteSha\"];\n const assetManifestSha = document[\"assetManifestSha\"];\n const rawResources = document[\"resources\"];\n if (!isPositiveInteger(themeId)) throw new Error(\"invalid theme id\");\n if (!isNonEmptyString(remoteSha)) throw new Error(\"invalid remote sha\");\n if (!isNonEmptyString(assetManifestSha)) {\n throw new Error(\"invalid asset manifest sha\");\n }\n if (!Array.isArray(rawResources)) throw new Error(\"invalid resources\");\n\n const resources = rawResources.map(parseResource);\n assertNoCaseCollisions(resources.map((resource) => resource.key));\n return { themeId, remoteSha, assetManifestSha, resources };\n}\n\nfunction parseResource(resource: unknown): RemoteResourceState {\n if (!isRecord(resource)) throw new Error(\"invalid resource\");\n\n const key = resource[\"key\"];\n const checksum = resource[\"checksum\"];\n const contentPresent = resource[\"contentPresent\"];\n const resourceType = resource[\"resourceType\"];\n const resourceId = resource[\"resourceId\"];\n const url = resource[\"url\"];\n if (!isNonEmptyString(key) || key.includes(\"\\0\")) {\n throw new Error(\"invalid resource key\");\n }\n if (checksum !== null && !isNonEmptyString(checksum)) {\n throw new Error(\"invalid resource checksum\");\n }\n if (typeof contentPresent !== \"boolean\") {\n throw new Error(\"invalid resource content marker\");\n }\n if (\n resourceType !== undefined &&\n resourceType !== null &&\n !isNonEmptyString(resourceType)\n ) {\n throw new Error(\"invalid resource type\");\n }\n if (\n resourceId !== undefined &&\n resourceId !== null &&\n !isPositiveInteger(resourceId)\n ) {\n throw new Error(\"invalid resource id\");\n }\n if (url !== undefined && url !== null && !isNonEmptyString(url)) {\n throw new Error(\"invalid resource url\");\n }\n\n return {\n key,\n checksum,\n contentPresent,\n resourceType,\n resourceId,\n url,\n };\n}\n\nfunction isRecord(candidate: unknown): candidate is Record<string, unknown> {\n return typeof candidate === \"object\" && candidate !== null;\n}\n\nfunction isNonEmptyString(candidate: unknown): candidate is string {\n return typeof candidate === \"string\" && candidate.length > 0;\n}\n\nfunction isPositiveInteger(candidate: unknown): candidate is number {\n return (\n typeof candidate === \"number\" &&\n Number.isInteger(candidate) &&\n candidate > 0\n );\n}\n","import { spawn } from \"node:child_process\";\nimport {\n existsSync,\n mkdirSync,\n mkdtempSync,\n readFileSync,\n rmSync,\n writeFileSync,\n} from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/**\n * A bare git repo hidden under `.fluid-theme/repo` that stands in for\n * git's index+HEAD when talking to the Fluid server. Every pull commits\n * the incoming remote state onto a single branch (`refs/heads/main`);\n * every successful push commits the outgoing local state onto the same\n * branch. HEAD therefore represents \"the last state the CLI and server\n * agreed on\", and its tree is the natural three-way-merge base for the\n * next pull.\n *\n * We stay in git's plumbing layer — no working tree, no index file\n * next to the theme content — so the shadow repo cannot interfere\n * with the user's own git repo (if they have one) around the theme\n * dir. All state lives inside `.fluid-theme/`.\n *\n * The class only wraps the small handful of plumbing commands we\n * actually need: hash-object, cat-file, write-tree (via a temp index),\n * commit-tree, update-ref, and merge-file. Everything else stays out.\n */\nexport class ShadowRepo {\n // Cached \"does HEAD exist\" result. `pull`/`push` call `blobAtHead`\n // per file (potentially hundreds of times per invocation), and\n // spawning a `git rev-parse` subprocess each time is measurable\n // overhead. HEAD presence only flips one way per instance —\n // `commitState` sets it true after a successful `update-ref` — so\n // the cache stays correct without invalidation.\n private headExists: boolean | undefined = undefined;\n\n private constructor(\n private readonly themeRoot: string,\n private readonly gitDir: string,\n ) {}\n\n /**\n * Return a ShadowRepo bound to `themeId` for the given theme root.\n * The shadow is initialized on first use and re-initialized when\n * the caller passes a themeId different from the one previously\n * recorded — the merge base is only meaningful for the theme it\n * was captured against, so a cross-theme operation (pull A → push\n * B, or two pulls of different themes into one dir) starts from a\n * clean slate rather than pretending B's state matches A's HEAD.\n */\n static async open(themeRoot: string, themeId: number): Promise<ShadowRepo> {\n const shadowDir = join(themeRoot, \".fluid-theme\");\n const gitDir = join(shadowDir, \"repo\");\n const themeIdFile = join(shadowDir, \"theme-id\");\n\n // The stored themeId is written on every commit, so a shadow with\n // a HEAD but no theme-id file is one written by an older CLI —\n // treat it as \"unknown\" and wipe rather than trust it against\n // whatever theme the caller is now operating on.\n if (existsSync(gitDir)) {\n const stored = readStoredThemeId(themeIdFile);\n if (stored !== themeId) {\n rmSync(gitDir, { recursive: true, force: true });\n }\n }\n\n const repo = new ShadowRepo(themeRoot, gitDir);\n\n if (!existsSync(gitDir)) {\n mkdirSync(shadowDir, { recursive: true });\n await repo.git([\"init\", \"--bare\", \"-b\", \"main\", gitDir], {\n cwd: themeRoot,\n });\n }\n\n // Rewrite even when the dir already existed — a fresh init above\n // won't have this file yet, and a preserved dir may have missed a\n // previous write if the process crashed mid-way. Idempotent.\n writeFileSync(themeIdFile, `${themeId}\\n`, \"utf-8\");\n\n // Idempotent: keep `.fluid-theme/` invisible to the user's own git\n // repo. The nested `.gitignore` handles `git add .` inside the\n // shadow dir; the root-level entry handles `git status` on the\n // theme root, which otherwise still lists the untracked shadow\n // directory itself (only its *contents* are ignored by the nested\n // file).\n const shadowIgnore = join(shadowDir, \".gitignore\");\n if (!existsSync(shadowIgnore)) {\n writeFileSync(\n shadowIgnore,\n \"# Fluid CLI shadow repo — internal state, not for version control.\\n*\\n\",\n );\n }\n await ensureRootGitignoreHidesShadow(themeRoot);\n\n return repo;\n }\n\n /** True when the repo has at least one commit on `refs/heads/main`. */\n async hasHead(): Promise<boolean> {\n if (this.headExists !== undefined) return this.headExists;\n try {\n await this.git([\"rev-parse\", \"--verify\", \"HEAD\"]);\n this.headExists = true;\n } catch {\n this.headExists = false;\n }\n return this.headExists;\n }\n\n /** Desktop snapshots deliberately use a different author. They are local\n * recovery points, not proof that those bytes were synced to the server. */\n async hasSyncedHead(): Promise<boolean> {\n if (!(await this.hasHead())) return false;\n const { stdout } = await this.git([\"log\", \"-1\", \"--format=%ae\"]);\n return stdout.toString(\"utf8\").trim() === \"cli@fluid.app\";\n }\n\n /**\n * Every path recorded under HEAD's tree, recursively. Callers use\n * this to detect local deletions (paths in HEAD, absent from the\n * working tree). Returns [] when HEAD has never been committed.\n */\n async headPaths(): Promise<string[]> {\n if (!(await this.hasHead())) return [];\n // -z is required, not cosmetic. Without it git honours core.quotePath\n // (default true) and renders any path containing a non-ASCII byte in\n // its C-quoted display form — `\"assets/caf\\303\\251.css\"`, quotes\n // included. That string matches nothing in the working tree, so\n // diffAgainstShadow reports the file as a local deletion and push\n // sends a DELETE for a key the server has never had. The failed\n // delete then blocks commitPushedState, so shadow HEAD never\n // advances and the phantom delete repeats on every later push.\n // -z also covers the rarer newline-in-path case; splitting on \"\\n\"\n // could not.\n const { stdout } = await this.git([\n \"ls-tree\",\n \"-r\",\n \"-z\",\n \"HEAD\",\n \"--name-only\",\n ]);\n return stdout\n .toString(\"utf8\")\n .split(\"\\0\")\n .filter((line) => line.length > 0);\n }\n\n /**\n * The content of `path` in HEAD's tree, or null when the path does\n * not exist there. Callers use this as the merge base for pull\n * conflict resolution.\n */\n async blobAtHead(path: string): Promise<Buffer | null> {\n if (!(await this.hasHead())) return null;\n try {\n const { stdout } = await this.git([\"cat-file\", \"-p\", `HEAD:${path}`]);\n return stdout;\n } catch {\n return null;\n }\n }\n\n /**\n * Write `content` as a blob in the shadow repo and return its sha.\n * Used by `commitState` to stage each file's content before the\n * `write-tree` call.\n */\n async writeBlob(content: string | Buffer): Promise<string> {\n const buf = typeof content === \"string\" ? Buffer.from(content) : content;\n const { stdout } = await this.git([\"hash-object\", \"-w\", \"--stdin\"], {\n input: buf,\n });\n return stdout.toString(\"utf8\").trim();\n }\n\n /**\n * Commit `files` as HEAD's new tree, threaded onto the current HEAD\n * as the parent. Uses a per-call temp index so a partial run can't\n * corrupt anything reachable from HEAD; the previous commit stays\n * intact until `update-ref` at the end.\n *\n * Returns the new commit sha.\n */\n async commitState(\n files: Array<{ path: string; sha: string }>,\n message: string,\n ): Promise<string> {\n const indexPath = mkdtempSync(join(tmpdir(), \"fluid-shadow-\")) + \"/index\";\n\n try {\n if (files.length > 0) {\n // Keep file lists off argv: large themes exceed Windows' process\n // command-line limit. NUL delimiters preserve paths verbatim.\n await this.git([\"update-index\", \"-z\", \"--index-info\"], {\n input: Buffer.from(\n files.map(({ path, sha }) => `100644 ${sha}\\t${path}\\0`).join(\"\"),\n ),\n env: { GIT_INDEX_FILE: indexPath },\n });\n }\n\n const treeSha = (\n await this.git([\"write-tree\"], { env: { GIT_INDEX_FILE: indexPath } })\n ).stdout\n .toString(\"utf8\")\n .trim();\n\n const parent = (await this.hasHead())\n ? (await this.git([\"rev-parse\", \"HEAD\"])).stdout.toString(\"utf8\").trim()\n : null;\n\n const commitArgs = [\"commit-tree\", treeSha, \"-m\", message];\n if (parent) commitArgs.push(\"-p\", parent);\n\n const commitSha = (\n await this.git(commitArgs, {\n env: {\n GIT_AUTHOR_NAME: \"Fluid CLI\",\n GIT_AUTHOR_EMAIL: \"cli@fluid.app\",\n GIT_COMMITTER_NAME: \"Fluid CLI\",\n GIT_COMMITTER_EMAIL: \"cli@fluid.app\",\n },\n })\n ).stdout\n .toString(\"utf8\")\n .trim();\n\n await this.git([\"update-ref\", \"refs/heads/main\", commitSha]);\n this.headExists = true;\n return commitSha;\n } finally {\n // Best-effort — a stale index in tmp is harmless.\n try {\n rmSync(indexPath, { force: true });\n rmSync(indexPath.substring(0, indexPath.length - \"/index\".length), {\n recursive: true,\n force: true,\n });\n } catch {\n // ignore\n }\n }\n }\n\n /**\n * Three-way merge of `local` against `remote` with `base` as the\n * common ancestor. Returns the merged bytes and a flag when\n * `git merge-file` reported unresolved conflicts (i.e. the output\n * contains `<<<<<<<` markers for the reader to resolve).\n *\n * `base` is null when HEAD has never seen this path; we merge\n * against an empty base, which is what git itself does for a new\n * file added on both sides.\n *\n * `favor` maps to `git merge-file`'s `--ours` / `--theirs`: instead\n * of emitting `<<<<<<<` markers, conflicting hunks are resolved to\n * the local (`\"local\"` → `--ours`, local is file1) or remote\n * (`\"remote\"` → `--theirs`) side. The output then never contains\n * markers, so the result is reported conflict-free even when\n * merge-file's exit code still counts the auto-resolved hunks.\n */\n async merge3(\n base: Buffer | null,\n local: Buffer,\n remote: Buffer,\n favor?: \"local\" | \"remote\",\n ): Promise<{ merged: Buffer; hasConflicts: boolean }> {\n const dir = mkdtempSync(join(tmpdir(), \"fluid-merge-\"));\n const localPath = join(dir, \"local\");\n const basePath = join(dir, \"base\");\n const remotePath = join(dir, \"remote\");\n\n try {\n writeFileSync(localPath, local);\n writeFileSync(basePath, base ?? Buffer.alloc(0));\n writeFileSync(remotePath, remote);\n\n // `git merge-file -p` writes the merged result to stdout instead\n // of mutating `localPath` in place, and exits non-zero (= number\n // of unresolved conflicts) when it couldn't reconcile everything.\n // We treat any non-zero exit as \"conflicts present\" as long as\n // stdout is populated — the caller writes it to disk with markers.\n try {\n const { stdout } = await this.git([\n \"merge-file\",\n \"-p\",\n ...(favor === \"local\"\n ? [\"--ours\"]\n : favor === \"remote\"\n ? [\"--theirs\"]\n : []),\n \"-L\",\n \"local\",\n \"-L\",\n \"base\",\n \"-L\",\n \"remote\",\n localPath,\n basePath,\n remotePath,\n ]);\n return { merged: stdout, hasConflicts: false };\n } catch (err) {\n const e = err as {\n code?: number;\n stdout?: Buffer | string;\n stderr?: Buffer | string;\n };\n // `git merge-file` exit codes:\n // 0 clean merge (handled in the try branch above)\n // 1..127 count of unresolved conflict hunks; stdout is the\n // merged content with markers — treat as conflict.\n // >=128 git itself crashed (SIGSEGV, out-of-memory) —\n // stdout is empty, don't clobber the user's file.\n // negative usage error (bad args) — same, don't clobber.\n // Requiring both the conflict-range code AND a non-empty\n // stdout matches the comment above and stops a crash from\n // silently writing 0 bytes to disk.\n const merged =\n e.stdout instanceof Buffer\n ? e.stdout\n : e.stdout != null\n ? Buffer.from(e.stdout)\n : Buffer.alloc(0);\n const inConflictRange =\n typeof e.code === \"number\" && e.code >= 1 && e.code <= 127;\n // With a favor side, merge-file resolved every hunk itself —\n // the exit code still counts them, but the output carries no\n // markers, so it's a finished merge from the caller's view.\n // The stdout-length guard is dropped here: an auto-resolved\n // result can be legitimately empty (favoring a side that\n // deleted all content).\n if (inConflictRange && favor) {\n return { merged, hasConflicts: false };\n }\n if (inConflictRange && merged.length > 0) {\n return { merged, hasConflicts: true };\n }\n throw err;\n }\n } finally {\n try {\n rmSync(dir, { recursive: true, force: true });\n } catch {\n // ignore\n }\n }\n }\n\n /**\n * Snapshot the working-tree copy of `paths` into HEAD as a single\n * commit. Intended for the migration path: on the first pull with\n * the new CLI (no shadow repo yet, but a `.fluid-theme.json` with\n * checksums exists) we seed HEAD with whatever is on disk before\n * running the merge, so unmodified files fast-forward cleanly and\n * modified files show a diff.\n */\n async seedFromWorkingTree(\n files: Array<{ path: string; content: string | Buffer }>,\n message: string,\n ): Promise<void> {\n const entries: Array<{ path: string; sha: string }> = [];\n for (const { path, content } of files) {\n entries.push({ path, sha: await this.writeBlob(content) });\n }\n await this.commitState(entries, message);\n }\n\n private async git(\n args: string[],\n opts: {\n cwd?: string;\n input?: Buffer;\n env?: Record<string, string>;\n } = {},\n ): Promise<{ stdout: Buffer; stderr: Buffer }> {\n const fullArgs =\n args[0] === \"init\" ? args : [\"--git-dir\", this.gitDir, ...args];\n const child = spawn(\"git\", fullArgs, {\n cwd: opts.cwd ?? this.themeRoot,\n env: { ...process.env, ...opts.env },\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n return new Promise((resolve, reject) => {\n const stdout: Buffer[] = [];\n const stderr: Buffer[] = [];\n child.stdout.on(\"data\", (chunk: Buffer) => stdout.push(chunk));\n child.stderr.on(\"data\", (chunk: Buffer) => stderr.push(chunk));\n child.on(\"error\", reject);\n child.stdin.on(\"error\", reject);\n child.on(\"close\", (code) => {\n const out = Buffer.concat(stdout);\n const err = Buffer.concat(stderr);\n if (code === 0) {\n resolve({ stdout: out, stderr: err });\n } else {\n const e = new Error(\n `git ${args.join(\" \")} exited with ${code}: ${err.toString(\"utf8\")}`,\n ) as Error & { code: number; stdout: Buffer; stderr: Buffer };\n e.code = code ?? -1;\n e.stdout = out;\n e.stderr = err;\n reject(e);\n }\n });\n child.stdin.end(opts.input);\n });\n }\n}\n\n/**\n * Content-type check the pull command uses to decide whether a file\n * is safe to run through `merge3` (line-based) or must fall back to\n * whole-file \"either/or\" resolution (binary).\n */\nexport function looksBinary(content: Buffer): boolean {\n // Same heuristic git uses for `core.autocrlf` detection: any NUL in\n // the first 8000 bytes counts as binary.\n const scan = content.subarray(0, Math.min(content.length, 8000));\n return scan.includes(0);\n}\n\n/** Best-effort readFile that returns null when the file does not exist. */\nexport function readIfExists(path: string): Buffer | null {\n try {\n return readFileSync(path);\n } catch {\n return null;\n }\n}\n\n/**\n * Parse the numeric theme id from `.fluid-theme/theme-id`. Returns\n * null when the file is missing or the contents don't parse cleanly;\n * `open` treats that as \"unknown theme\" and rebuilds the shadow.\n */\nfunction readStoredThemeId(themeIdFile: string): number | null {\n try {\n const stored = parseInt(readFileSync(themeIdFile, \"utf-8\").trim(), 10);\n return Number.isFinite(stored) ? stored : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Append `.fluid-theme/` to the theme root's `.gitignore` when the\n * theme dir sits inside a git working tree and the entry isn't\n * already there. Skipped when the user isn't in a git repo — no\n * point manufacturing a `.gitignore` for someone who doesn't use\n * git. Idempotent — a second call is a no-op.\n */\nasync function ensureRootGitignoreHidesShadow(\n themeRoot: string,\n): Promise<void> {\n const insideGit = await new Promise<boolean>((resolve) => {\n const child = spawn(\"git\", [\"rev-parse\", \"--is-inside-work-tree\"], {\n cwd: themeRoot,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n child.on(\"close\", (code) => resolve(code === 0));\n child.on(\"error\", () => resolve(false));\n });\n if (!insideGit) return;\n\n const gitignorePath = join(themeRoot, \".gitignore\");\n let existing = \"\";\n try {\n existing = readFileSync(gitignorePath, \"utf-8\");\n } catch {\n existing = \"\";\n }\n\n const lines = existing.split(\"\\n\").map((line) => line.trim());\n // Match both `.fluid-theme/` and `.fluid-theme` — either shape hides\n // the dir from `git status`.\n if (lines.includes(\".fluid-theme/\") || lines.includes(\".fluid-theme\")) {\n return;\n }\n\n const separator =\n existing.length === 0 || existing.endsWith(\"\\n\") ? \"\" : \"\\n\";\n writeFileSync(\n gitignorePath,\n `${existing}${separator}.fluid-theme/\\n`,\n \"utf-8\",\n );\n}\n","import chalk from \"chalk\";\nimport prompts from \"prompts\";\nimport type { createApiClient } from \"./api.js\";\nimport { themes, type components } from \"@fluid-app/themes-api-client\";\n\nexport type ApplicationTheme = components[\"schemas\"][\"ApplicationTheme\"];\n\nconst PAGE_SIZE = 50;\nconst LOAD_MORE_VALUE = -1;\n\nfunction themeLabel(t: ApplicationTheme): string {\n const active = t.status === \"active\" ? ` ${chalk.green(\"[active]\")}` : \"\";\n return `${t.name} (#${t.id})${active}`;\n}\n\nfunction themeChoices(\n themeList: ApplicationTheme[],\n hasMore: boolean,\n): prompts.Choice[] {\n const choices: prompts.Choice[] = themeList.map((t) => ({\n title: themeLabel(t),\n value: t.id,\n }));\n if (hasMore) {\n choices.push({\n title: chalk.dim(`── Load more themes ──`),\n value: LOAD_MORE_VALUE,\n });\n }\n return choices;\n}\n\nasync function fetchThemesPage(\n api: ReturnType<typeof createApiClient>,\n page: number,\n searchQuery?: string,\n): Promise<{\n themes: ApplicationTheme[];\n hasMore: boolean;\n}> {\n const body = await themes.listApplicationThemes(api, {\n per_page: PAGE_SIZE,\n page,\n ...(searchQuery ? { search_query: searchQuery } : {}),\n });\n const list = body.application_themes ?? [];\n const totalPages = body.meta?.total_pages ?? 1;\n return { themes: list, hasMore: page < totalPages };\n}\n\nexport async function selectTheme(\n api: ReturnType<typeof createApiClient>,\n message: string,\n): Promise<ApplicationTheme> {\n const allThemes: ApplicationTheme[] = [];\n let page = 1;\n let hasMore = true;\n let initialIndex = 0;\n\n // Search cache — persists across suggest calls\n let searchQuery = \"\";\n let searchResults: ApplicationTheme[] = [];\n\n while (true) {\n if (hasMore && allThemes.length < page * PAGE_SIZE) {\n const result = await fetchThemesPage(api, page);\n allThemes.push(...result.themes);\n hasMore = result.hasMore;\n }\n\n if (!allThemes.length) {\n console.error(\"No themes found.\");\n process.exit(1);\n }\n\n const choices = themeChoices(allThemes, hasMore);\n\n const { id } = await prompts(\n {\n type: \"autocomplete\",\n name: \"id\",\n message,\n initial: initialIndex,\n choices,\n suggest: async (input: string, choices: prompts.Choice[]) => {\n if (!input) {\n searchQuery = \"\";\n searchResults = [];\n return choices;\n }\n\n if (input !== searchQuery) {\n searchQuery = input;\n try {\n const result = await fetchThemesPage(api, 1, input);\n searchResults = result.themes;\n } catch {\n searchResults = [];\n }\n }\n\n return searchResults.map((t) => ({\n title: themeLabel(t),\n value: t.id,\n }));\n },\n },\n { onCancel: () => process.exit(130) },\n );\n\n if (id === LOAD_MORE_VALUE) {\n initialIndex = allThemes.length;\n page++;\n continue;\n }\n\n if (!id) {\n console.error(\"No theme selected.\");\n process.exit(1);\n }\n\n // Check loaded themes first, then search results\n const found =\n allThemes.find((t) => t.id === id) ??\n searchResults.find((t) => t.id === id);\n if (found) return found;\n\n // Fetch directly by ID as fallback\n const body = await themes.getApplicationTheme(api, id);\n return body.application_theme;\n }\n}\n\nexport async function findTheme(\n api: ReturnType<typeof createApiClient>,\n identifier: string,\n): Promise<ApplicationTheme> {\n // Try ID lookup first\n const idNum = Number(identifier);\n if (Number.isInteger(idNum) && idNum > 0) {\n try {\n const body = await themes.getApplicationTheme(api, idNum);\n if (body.application_theme) return body.application_theme;\n } catch {\n // Not found by ID, fall through to search\n }\n }\n\n // Search by name via API with pagination\n let page = 1;\n let hasMore = true;\n while (hasMore) {\n const result = await fetchThemesPage(api, page, identifier);\n const found = result.themes.find(\n (t) => t.name.toLowerCase() === identifier.toLowerCase(),\n );\n if (found) return found;\n hasMore = result.hasMore;\n page++;\n }\n\n console.error(`No theme found with identifier: ${identifier}`);\n process.exit(1);\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join, relative, resolve, sep } from \"node:path\";\n\nexport interface FluidWorkspace {\n /** Absolute path to the workspace root (where .fluid-workspace.json lives) */\n root: string;\n /** Parsed workspace config */\n config: WorkspaceConfig;\n}\n\ninterface WorkspaceConfig {\n type: string;\n version: number;\n}\n\nconst WORKSPACE_FILE = \".fluid-workspace.json\";\n\n/**\n * Walk up from `startDir` looking for `.fluid-workspace.json`.\n * Returns the workspace info if found, or `null` if not in a workspace.\n */\nexport function findWorkspace(startDir?: string): FluidWorkspace | null {\n let dir = resolve(startDir ?? process.cwd());\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const candidate = join(dir, WORKSPACE_FILE);\n if (existsSync(candidate)) {\n try {\n const raw = readFileSync(candidate, \"utf-8\");\n const config = JSON.parse(raw) as WorkspaceConfig;\n return { root: dir, config };\n } catch {\n return null;\n }\n }\n const parent = dirname(dir);\n if (parent === dir) break; // reached filesystem root\n dir = parent;\n }\n\n return null;\n}\n\n/**\n * If cwd is already inside `{workspace}/local/{company}/...`, return that\n * theme root directory. Otherwise return null.\n *\n * Examples (workspace root = /code/fluid-theme-dev):\n * cwd = /code/fluid-theme-dev/local/acme-co → /code/fluid-theme-dev/local/acme-co\n * cwd = /code/fluid-theme-dev/local/acme-co/templates → /code/fluid-theme-dev/local/acme-co\n * cwd = /code/fluid-theme-dev → null\n * cwd = /code/fluid-theme-dev/local → null\n */\nexport function resolveThemeRootFromCwd(\n workspace: FluidWorkspace,\n): string | null {\n const cwd = resolve(process.cwd());\n const localDir = join(workspace.root, \"local\");\n const rel = relative(localDir, cwd);\n\n // Not under local/ at all, or exactly at local/\n if (rel.startsWith(\"..\") || rel === \".\") return null;\n\n // rel is like \"acme-co\" or \"acme-co/templates/subfolder\"\n // The theme root is the first segment: local/{company}\n const firstSegment = rel.split(sep)[0];\n if (!firstSegment) return null;\n\n return join(localDir, firstSegment);\n}\n","import { Command } from \"commander\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { readThemeConfig } from \"../theme-config.js\";\nimport {\n devThemeKey,\n getDevTheme,\n setDevTheme,\n setLastDevThemeId,\n clearDevTheme,\n type DevThemeRef,\n} from \"../plugin-state.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { startDevServer } from \"../theme/dev-server/index.js\";\nimport { ThemeAssetManifest } from \"../theme/asset-manifest.js\";\nimport {\n devRemoteStateFromSourceShadow,\n readDevRemoteBaseline,\n removeDevRemoteBaseline,\n writeDevRemoteBaseline,\n} from \"../theme/dev-remote-baseline.js\";\nimport { ShadowRepo } from \"../theme/shadow-repo.js\";\nimport {\n checkPortAvailable,\n PortInUseError,\n} from \"../theme/dev-server/port-preflight.js\";\nimport { isApiError, themes } from \"@fluid-app/themes-api-client\";\nimport { findTheme, type ApplicationTheme } from \"../theme-picker.js\";\nimport { findWorkspace, resolveThemeRootFromCwd } from \"../workspace.js\";\n\ninterface CompanyMe {\n data: { company: { subdomain?: string; name?: string } };\n}\n\n/** Whether this invocation may read and persist the dev remote baseline. */\nexport function devRemoteBaselineEnabled(explicitTheme: boolean): boolean {\n return (\n !explicitTheme && process.env[\"FLUID_THEME_DEV_DISABLE_SHADOW_SYNC\"] !== \"1\"\n );\n}\n\n/**\n * Create the isolated theme used by `theme dev`.\n *\n * A checkout from `theme pull` has a source theme id. New servers clone that\n * source by reference, preserving its DAM/ImageKit assets without moving\n * bytes. A 404/405 keeps older deployments compatible with the established\n * empty-theme flow; other failures must remain visible to the developer.\n */\nexport async function createDevelopmentTheme(\n api: ReturnType<typeof createApiClient>,\n sourceThemeId: number | undefined,\n name: string,\n): Promise<{ theme: ApplicationTheme; referenceCloned: boolean }> {\n if (sourceThemeId !== undefined) {\n try {\n const body = await themes.cloneApplicationThemeForDevelopment(\n api,\n sourceThemeId,\n { application_theme: { name } },\n );\n return { theme: body.application_theme, referenceCloned: true };\n } catch (error) {\n if (\n !isApiError(error) ||\n (error.status !== 404 && error.status !== 405)\n ) {\n throw error;\n }\n\n console.warn(\n \"Server-side theme cloning is unavailable; falling back to an empty development theme. The first sync may take longer.\",\n );\n }\n }\n\n const body = await themes.createApplicationTheme(api, {\n application_theme: { name, status: \"development\" },\n });\n return { theme: body.application_theme, referenceCloned: false };\n}\n\nasync function ensureDevTheme(\n api: ReturnType<typeof createApiClient>,\n projectKey: string,\n identifier?: string,\n sourceThemeId?: number,\n): Promise<{ theme: ApplicationTheme; referenceCloned: boolean }> {\n if (identifier) {\n const theme = await findTheme(api, identifier);\n // Keep `navigate` pointed at whatever the dev server is actually serving.\n setLastDevThemeId(theme.id);\n return { theme, referenceCloned: false };\n }\n\n // Reuse this project's stored dev theme if it still exists and is still a\n // development theme (a published/promoted theme must not be edited in place).\n const stored = getDevTheme(projectKey);\n // A checkout pulled from a different source needs an isolated sandbox of\n // its own. Older stored entries have no source id, so they are retained only\n // for worktrees that were not pulled from a remote theme.\n if (stored && stored.sourceThemeId === sourceThemeId) {\n try {\n const body = await themes.getApplicationTheme(api, stored.id);\n const existing = body.application_theme;\n if (existing && existing.status === \"development\") {\n console.log(`Using existing dev theme #${existing.id}`);\n // Refresh the stored name and mark it most-recent for `navigate`.\n setDevTheme(projectKey, {\n ...stored,\n id: existing.id,\n name: existing.name,\n ...(sourceThemeId === undefined ? {} : { sourceThemeId }),\n });\n return { theme: existing, referenceCloned: false };\n }\n } catch {\n // Theme no longer exists — fall through to create a new one.\n }\n // Stored theme is gone or no longer a dev theme; forget it.\n clearDevTheme(projectKey);\n }\n\n // Create a new development theme\n const { hostname } = await import(\"node:os\");\n const host = hostname().split(\".\")[0] ?? \"dev\";\n const name =\n `Development (${host}-${Math.random().toString(36).slice(2, 8)})`.slice(\n 0,\n 50,\n );\n\n const creation = await createDevelopmentTheme(api, sourceThemeId, name);\n const { theme } = creation;\n const devTheme: DevThemeRef = {\n id: theme.id,\n name: theme.name,\n ...(sourceThemeId === undefined ? {} : { sourceThemeId }),\n };\n setDevTheme(projectKey, devTheme);\n console.log(`Created dev theme: ${theme.name} (#${theme.id})`);\n return creation;\n}\n\nexport function createDevCommand(): Command {\n return new Command(\"dev\")\n .description(\"Start the theme dev server with hot reload\")\n .option(\"--host <host>\", \"Local server host\", \"127.0.0.1\")\n .option(\"--port <port>\", \"Local server port\", \"9292\")\n .option(\n \"-t, --theme <name-or-id>\",\n \"Use an existing theme instead of dev theme\",\n )\n .option(\"-f, --force\", \"Skip schema validation on upload\")\n .option(\"--live-reload <mode>\", \"Reload mode: full-page | off\", \"full-page\")\n .option(\"--navigate\", \"Open browser navigator after server starts\")\n .option(\"--root <path>\", \"Theme root directory\", \".\")\n .action(\n async (opts: {\n host: string;\n port: string;\n theme?: string;\n force?: boolean;\n liveReload: string;\n navigate?: boolean;\n root: string;\n }) => {\n requireToken();\n\n // If no explicit --root and we're inside a workspace, resolve to the theme root\n let rootPath = opts.root;\n if (rootPath === \".\") {\n const workspace = findWorkspace();\n if (workspace) {\n rootPath = resolveThemeRootFromCwd(workspace) ?? rootPath;\n }\n }\n\n const themeRoot = new ThemeRoot(rootPath);\n if (!themeRoot.isValid()) {\n console.error(`'${rootPath}' does not look like a theme directory.`);\n process.exit(1);\n }\n\n const port = Number(opts.port);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n console.error(\n `Invalid port: '${opts.port}'. Must be an integer between 1 and 65535.`,\n );\n process.exit(1);\n }\n\n // Fail fast if the port is already taken — everything below this\n // point (resolving/creating the dev theme, the initial sync) is\n // expensive and would otherwise run to completion only to crash\n // with a raw EADDRINUSE stack trace when the server finally binds.\n try {\n await checkPortAvailable(opts.host, port);\n } catch (e) {\n if (e instanceof PortInUseError) {\n console.error(e.message);\n } else {\n console.error(`Failed to check port availability: ${e}`);\n }\n process.exit(1);\n }\n\n const reloadMode = opts.liveReload === \"off\" ? \"off\" : \"full-page\";\n const api = createApiClient();\n const config = readThemeConfig(themeRoot.root);\n\n // Use company from .fluid-theme.json if available, otherwise fetch\n let company: string;\n if (config?.company) {\n company = config.company;\n } else {\n const companyRes = await api.get<CompanyMe>(\n \"/api/company/v1/companies/me\",\n );\n company = companyRes.data?.company?.subdomain ?? \"\";\n if (!company) {\n console.error(\n \"Could not determine company subdomain. Make sure your token is valid.\",\n );\n process.exit(1);\n }\n }\n\n // Always iterate on an isolated dev theme: reuse the stored one or\n // create a fresh `development` theme. `--theme` is the explicit\n // escape hatch for targeting an existing theme. A pulled theme id is\n // never a sync target: it only seeds a new isolated reference clone.\n const projectKey = devThemeKey(company, themeRoot.root);\n const devTarget = opts.theme\n ? await ensureDevTheme(api, projectKey, opts.theme)\n : await ensureDevTheme(api, projectKey, undefined, config?.themeId);\n const { theme } = devTarget;\n const assetManifest = new ThemeAssetManifest(themeRoot.root);\n const baselineEnabled = devRemoteBaselineEnabled(Boolean(opts.theme));\n let initialRemoteState = baselineEnabled\n ? readDevRemoteBaseline(\n themeRoot.root,\n theme.id,\n assetManifest.fingerprint(),\n )\n : null;\n if (\n !initialRemoteState &&\n baselineEnabled &&\n devTarget.referenceCloned &&\n config?.themeId &&\n config.baseSha\n ) {\n try {\n const sourceShadow = await ShadowRepo.open(\n themeRoot.root,\n config.themeId,\n );\n initialRemoteState = await devRemoteStateFromSourceShadow(\n themeRoot,\n sourceShadow,\n theme.id,\n config.baseSha,\n );\n if (initialRemoteState) {\n writeDevRemoteBaseline(themeRoot.root, initialRemoteState);\n }\n } catch {\n initialRemoteState = null;\n }\n }\n const editorUrl = `https://admin.fluid.app/themes/${theme.id}/editor`;\n\n let stop: (() => void) | undefined;\n\n const cleanup = () => {\n stop?.();\n process.exit(0);\n };\n process.on(\"SIGINT\", cleanup);\n process.on(\"SIGTERM\", cleanup);\n\n stop = await startDevServer(\n api,\n {\n id: theme.id,\n name: theme.name,\n company,\n editorUrl,\n },\n themeRoot,\n {\n host: opts.host,\n port,\n reloadMode,\n validate: !opts.force,\n ...(initialRemoteState ? { initialSync: initialRemoteState } : {}),\n ...(baselineEnabled\n ? {\n onRemoteState: (state) =>\n writeDevRemoteBaseline(themeRoot.root, state),\n onRemoteStateInvalidated: () =>\n removeDevRemoteBaseline(themeRoot.root),\n }\n : {}),\n },\n (address) => {\n console.log(`\\n Dev server: ${address}`);\n console.log(` Web editor: ${editorUrl}`);\n console.log(\"\\n Watching for file changes…\\n\");\n\n if (opts.navigate) {\n import(\"open\").then((m) => m.default(`${address}/home`));\n }\n },\n );\n\n // Keep process alive\n await new Promise(() => {});\n },\n );\n}\n","import type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport {\n MANAGED_ASSET_SHADOW_SENTINEL,\n ThemeAssetManifest,\n} from \"./asset-manifest.js\";\nimport { ShadowRepo, readIfExists } from \"./shadow-repo.js\";\nimport { isStylesheetKey } from \"./stylesheet-keys.js\";\nimport { assertNoCaseCollisions } from \"./case-collisions.js\";\n\nexport interface DiffSet {\n /** Files whose local content differs from shadow HEAD — the ones to send. */\n changed: ThemeFile[];\n /** Paths present in shadow HEAD but no longer on disk — deletions to send. */\n deleted: string[];\n}\n\n/**\n * Compute what changed locally since the last time the shadow repo\n * committed a state. Replaces the sha256 `checksums` map: shadow HEAD\n * is the source of truth for \"what the CLI last saw the server have\".\n *\n * Files whose local bytes are byte-identical to their HEAD blob are\n * skipped; anything else — new, modified, or a locally-deleted path\n * that HEAD still has — is included.\n */\nexport async function diffAgainstShadow(\n themeRoot: ThemeRoot,\n shadow: ShadowRepo,\n): Promise<DiffSet> {\n const changed: ThemeFile[] = [];\n const deleted: string[] = [];\n\n const localFiles = themeRoot.files();\n const localByKey = new Map<string, ThemeFile>();\n const assetManifest = new ThemeAssetManifest(themeRoot.root);\n let headPaths: string[];\n try {\n headPaths = await shadow.headPaths();\n } catch (error) {\n throw new Error(\"Could not read the local theme shadow\", { cause: error });\n }\n\n assertNoCaseCollisions([\n ...localFiles.map((file) => file.relativePath),\n ...headPaths,\n ]);\n const headPathSet = new Set(headPaths);\n\n for (const file of localFiles) {\n if (!file.exists) continue;\n localByKey.set(file.relativePath, file);\n\n const headBlob = await shadow.blobAtHead(file.relativePath);\n if (!headBlob && headPathSet.has(file.relativePath)) {\n throw new Error(\n `Could not read the local theme shadow: ${file.relativePath}`,\n );\n }\n const localBuf = file.isText ? Buffer.from(file.read()) : file.readBinary();\n if (headBlob && headBlob.equals(localBuf)) continue;\n\n changed.push(file);\n }\n\n for (const key of headPaths) {\n if (localByKey.has(key)) continue;\n if (themeRoot.ignore.ignore(key)) continue;\n // A manifest-backed asset is deliberately absent from disk. Its URL is\n // still authoritative and must never be emitted as a remote deletion.\n if (assetManifest.has(key)) continue;\n if (isStylesheetKey(key)) continue; // hidden from the API surface\n deleted.push(key);\n }\n\n return { changed, deleted };\n}\n\n/**\n * Refuse a push when any working file still contains a conflict marker\n * from a previous pull. Mirrors git's \"you have unresolved conflicts;\n * fix them and re-run\" behavior — the whole point of writing markers\n * on pull was to hand resolution to the user, so we can't send them\n * upstream.\n */\nexport function findUnresolvedConflicts(files: ThemeFile[]): string[] {\n const flagged: string[] = [];\n for (const file of files) {\n if (!file.isText) continue;\n const buf = readIfExists(file.absolutePath);\n if (!buf) continue;\n if (containsConflictMarker(buf)) flagged.push(file.relativePath);\n }\n return flagged;\n}\n\n/**\n * Stable, machine-readable one-liner for non-interactive callers\n * (Mist Desktop's publish flow parses push output). Uploading marker-\n * bearing files to a live theme is never acceptable, so `--auto-\n * baseline` pushes still refuse — but they emit this line so the\n * desktop can surface WHICH files block the publish instead of a\n * dead-end wall of prose. Format:\n *\n * FLUID_THEME_PUSH_BLOCKED code=conflict_markers files=a.liquid,b.json\n */\nexport function conflictMarkerBlockLine(files: string[]): string {\n return `FLUID_THEME_PUSH_BLOCKED code=conflict_markers files=${files.join(\",\")}`;\n}\n\nconst CONFLICT_START = Buffer.from(\"<<<<<<<\");\nconst CONFLICT_MID = Buffer.from(\"=======\");\nconst CONFLICT_END = Buffer.from(\">>>>>>>\");\n\n/**\n * A file counts as unresolved when it contains all three marker\n * shapes: `<<<<<<<`, `=======`, and `>>>>>>>`. Requiring all three\n * avoids false positives — a line of equals signs alone (e.g. inside\n * an ASCII table in a template comment) doesn't trip the guard.\n */\nfunction containsConflictMarker(buf: Buffer): boolean {\n return (\n buf.includes(CONFLICT_START) &&\n buf.includes(CONFLICT_MID) &&\n buf.includes(CONFLICT_END)\n );\n}\n\n/**\n * Commit the current working-tree state to shadow HEAD after a\n * successful push. Ensures the next pull's merge base is the state\n * we know the server just accepted.\n */\nexport async function commitPushedState(\n themeRoot: ThemeRoot,\n shadow: ShadowRepo,\n message: string,\n): Promise<void> {\n const entries: Array<{ path: string; sha: string }> = [];\n const assetManifest = new ThemeAssetManifest(themeRoot.root);\n const localKeys = new Set<string>();\n for (const file of themeRoot.files()) {\n if (!file.exists) continue;\n localKeys.add(file.relativePath);\n // A manifest entry normally has no file at all. If bytes are present (for\n // example after a metadata fallback), they remain authoritative until a\n // successful upload can externalize them again.\n const buf = file.isText ? Buffer.from(file.read()) : file.readBinary();\n entries.push({ path: file.relativePath, sha: await shadow.writeBlob(buf) });\n }\n let managedAssetSentinelSha: string | undefined;\n for (const key of assetManifest.keys()) {\n if (localKeys.has(key)) continue;\n managedAssetSentinelSha ??= await shadow.writeBlob(\n MANAGED_ASSET_SHADOW_SENTINEL,\n );\n entries.push({\n path: key,\n sha: managedAssetSentinelSha,\n });\n }\n // Commit an empty tree too when HEAD exists: a successful final deletion\n // must clear its old path/sentinel so later pushes do not repeat it.\n if (entries.length > 0 || (await shadow.hasHead())) {\n await shadow.commitState(entries, message);\n }\n}\n\n/**\n * A target can already have every URL-backed FileResource (for example from a\n * reference clone), while this checkout's manifest still names its old source\n * theme. Adopt the target and clear any legacy binary from shadow even though\n * no remote write was necessary.\n */\nexport async function finalizeManifestOnlyPush(\n syncer: { repointManagedAssetsToCurrentTheme(): void },\n themeRoot: ThemeRoot,\n shadow: ShadowRepo,\n): Promise<void> {\n syncer.repointManagedAssetsToCurrentTheme();\n await commitPushedState(\n themeRoot,\n shadow,\n `push @ ${new Date().toISOString()}`,\n );\n}\n","import { sep } from \"node:path\";\nimport type { components } from \"@fluid-app/themes-api-client\";\nimport type { ShadowRepo } from \"./shadow-repo.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport type { FetchBinary } from \"./merge-pull.js\";\n\ntype RemoteResource = components[\"schemas\"][\"ApplicationThemeResource\"];\n\nexport interface SeedBaselineResult {\n /** False when a baseline already existed (HEAD present) — e.g. the\n * legacy checksum-era migration already seeded it. The migration\n * always wins over server seeding: a migrated HEAD carries the\n * local \"unmodified since last sync\" blobs, which is a better\n * merge base than the server's current state. */\n seeded: boolean;\n /** Paths recorded into the baseline commit. */\n recorded: string[];\n /** Server paths with no local counterpart, deliberately left OUT of\n * the baseline (see invariant below). */\n serverOnly: string[];\n /** Non-fatal per-file problems (e.g. a binary download failed). */\n errors: string[];\n}\n\n/**\n * `fluid theme push --auto-baseline`: when a theme directory has no\n * shadow baseline (scaffold that never pulled, or a dir last synced\n * by the checksum-era CLI whose migration had nothing to seed),\n * record the server's CURRENT state as the baseline commit so the\n * normal push diff (local vs baseline) can run.\n *\n * Invariants:\n *\n * 1. **The working tree is never touched.** Baseline recording writes\n * blobs into the bare shadow repo only; not a single byte on disk\n * changes. Local files identical to the server simply won't diff;\n * files that differ (or exist only locally) will push.\n *\n * 2. **Server-only files are never deleted.** Paths that exist on the\n * server but not locally are deliberately excluded from the\n * baseline commit. `diffAgainstShadow` reports deletions as \"in\n * HEAD but not on disk\", so putting server-only paths into HEAD\n * would mark them for remote deletion — on THIS push (or worse, a\n * later one) — for files the user never had. Excluding them makes\n * the diff structurally unable to delete them; the next `pull`\n * materializes them locally and records them for real.\n *\n * Bandwidth note: a server file whose sha256 checksum matches the\n * local file's is recorded from the LOCAL bytes (identical by\n * definition), so binary assets that are already in sync are never\n * downloaded just to seed the baseline.\n *\n * No-op (`seeded: false`) when HEAD already exists.\n */\nexport async function seedBaselineFromServer(input: {\n shadow: ShadowRepo;\n themeRoot: ThemeRoot;\n remote: RemoteResource[];\n fetchBinary: FetchBinary;\n message: string;\n}): Promise<SeedBaselineResult> {\n const { shadow, themeRoot, remote, fetchBinary, message } = input;\n const result: SeedBaselineResult = {\n seeded: false,\n recorded: [],\n serverOnly: [],\n errors: [],\n };\n\n if (await shadow.hasHead()) return result;\n\n const entries: Array<{ path: string; sha: string }> = [];\n\n for (const resource of remote) {\n const file = themeRoot.file(resource.key);\n\n // Same traversal guard as mergePull — never trust remote keys.\n if (!file.absolutePath.startsWith(themeRoot.root + sep)) {\n result.errors.push(`Baseline ${resource.key}: path traversal detected`);\n continue;\n }\n\n if (!file.exists) {\n result.serverOnly.push(resource.key);\n continue;\n }\n\n let content: Buffer | null;\n if (resource.checksum && file.checksum() === resource.checksum) {\n // Identical to the server — local bytes ARE the server bytes.\n content = file.isText ? Buffer.from(file.read()) : file.readBinary();\n } else {\n try {\n content = await materializeRemote(resource, fetchBinary);\n } catch (e) {\n result.errors.push(\n `Baseline ${resource.key}: ${e instanceof Error ? e.message : String(e)}`,\n );\n continue;\n }\n }\n if (content == null) {\n // Nothing to record (empty resource) — the local file will be\n // treated as new and pushed, which is the honest outcome.\n continue;\n }\n\n entries.push({ path: resource.key, sha: await shadow.writeBlob(content) });\n result.recorded.push(resource.key);\n }\n\n if (entries.length > 0) {\n await shadow.commitState(entries, message);\n result.seeded = true;\n }\n\n return result;\n}\n\nasync function materializeRemote(\n resource: RemoteResource,\n fetchBinary: FetchBinary,\n): Promise<Buffer | null> {\n if (resource.resource_type === \"FileResource\" && resource.url) {\n return fetchBinary(resource.url);\n }\n if (resource.content == null) return null;\n const text =\n typeof resource.content === \"string\"\n ? resource.content\n : JSON.stringify(resource.content);\n return Buffer.from(text);\n}\n","import { readLegacyThemeConfig } from \"../theme-config.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport type { ShadowRepo } from \"./shadow-repo.js\";\n\n/**\n * On first pull after upgrading from a checksum-era CLI, the shadow\n * repo starts with no HEAD. `mergePull` would then run every diverged\n * file through a null-base merge — even files the user never touched\n * locally — producing spurious `<<<<<<<` markers for every file the\n * server updated since the last pull.\n *\n * Recover a real merge base by trusting the legacy sha256 checksums:\n * any local file whose content still matches its stored checksum is\n * \"unmodified since last pull\" and can be committed as HEAD. Files\n * whose local sha256 diverges from the stored checksum stay\n * unseeded — we don't have their pre-modification content, so a\n * null-base merge (marker-first UX) is the honest fallback for them.\n *\n * Idempotent: no-op when HEAD already exists, when there is no\n * legacy config, when the config is for a different theme, or when\n * the checksums map is empty. Runs before `mergePull` so its base\n * lookups see the seeded tree.\n */\nexport async function migrateLegacyChecksumsIntoShadow(input: {\n shadow: ShadowRepo;\n themeRoot: ThemeRoot;\n absoluteRoot: string;\n themeId: number;\n}): Promise<void> {\n const { shadow, themeRoot, absoluteRoot, themeId } = input;\n\n if (await shadow.hasHead()) return;\n\n const legacy = readLegacyThemeConfig(absoluteRoot);\n if (!legacy) return;\n if (legacy.themeId !== themeId) return;\n if (!legacy.checksums || Object.keys(legacy.checksums).length === 0) return;\n\n const seed: Array<{ path: string; content: Buffer }> = [];\n for (const file of themeRoot.files()) {\n if (!file.exists) continue;\n const stored = legacy.checksums[file.relativePath];\n if (!stored) continue;\n if (file.checksum() !== stored) continue;\n\n const content = file.isText ? Buffer.from(file.read()) : file.readBinary();\n seed.push({ path: file.relativePath, content });\n }\n\n if (seed.length === 0) return;\n\n await shadow.seedFromWorkingTree(\n seed,\n `migrate from checksum-era CLI @ ${new Date().toISOString()}`,\n );\n}\n","import type { ApiClient } from \"../api.js\";\nimport type { ChangeEntry, GitSyncActor } from \"@fluid-app/fluid-cli\";\nimport {\n gitSyncActorFromMe,\n gitSyncCommitSubject,\n resolveGitSyncActor,\n summarizeChanges,\n} from \"@fluid-app/fluid-cli\";\n\n/**\n * User-stamped shadow-repo commit subjects, in the canonical GitSync\n * format shared with the mist / portal / widget CLIs and the Ruby\n * adapters (see `@fluid-app/fluid-cli`'s git-sync/commit-subject.ts):\n *\n * Mike Tingey: Push theme Aurora · 2026-07-22 05:10:33 (0182d21e-…)\n *\n * When the actor is unknown (offline, /api/me down, token weirdness) the\n * commit is attributed to `Fluid` in the same shape, so consumers need\n * one grammar rather than two:\n *\n * Fluid: Push theme Aurora · 2026-07-22 05:10:33\n *\n * Shadow repos written by older CLIs still hold the pre-format machine\n * wording (`push @ <iso>`). We no longer emit it, but Mist Desktop's\n * History popover still parses it so those repos keep rendering.\n */\n\nexport type SyncVerb =\n | \"Push\"\n | \"Pull\"\n | \"Baseline from server\"\n | \"Snapshot before pull\";\n\n/** Human action per verb, used as the canonical subject's action. */\nconst BASE_MESSAGE: Record<SyncVerb, string> = {\n Push: \"Push from Fluid CLI\",\n Pull: \"Pull from Fluid CLI\",\n \"Baseline from server\": \"Baseline from server\",\n \"Snapshot before pull\": \"Snapshot before pull\",\n};\n\n/** Re-exported under the local name the theme CLI has always used. */\nexport type SyncActor = GitSyncActor;\n\n/**\n * Build a shadow-repo commit subject.\n *\n * `changes` (when supplied) replaces the per-verb constant with a\n * description of what the sync actually carried, so the shadow log reads\n * `Update 3 files in templates` rather than `Push from Fluid CLI` on\n * every entry. Falls back to the constant for an empty change set — a\n * baseline or a no-op sync still deserves a sensible subject.\n */\nexport function syncCommitSubject(\n verb: SyncVerb,\n actor: SyncActor | null,\n when: Date = new Date(),\n changes: readonly ChangeEntry[] = [],\n): string {\n const action = summarizeChanges(changes) ?? BASE_MESSAGE[verb];\n return gitSyncCommitSubject(action, actor, when);\n}\n\n/** Resolve a `/api/me` body to a commit actor. Exported for tests.\n * Delegates to the shared resolver so the name fallback chain\n * (full_name → first+last → email → `user-<id>`) can't drift between\n * CLIs. Null when the body carries no usable identity — the caller then\n * writes a `Fluid:` system commit. */\nexport function actorFromMe(\n raw: Parameters<typeof gitSyncActorFromMe>[0],\n): SyncActor | null {\n return gitSyncActorFromMe(raw);\n}\n\n/**\n * Best-effort fetch of the signed-in user for commit stamping. Races\n * `/api/me` against a short timeout and NEVER rejects — a sync must\n * not block or fail because the identity lookup did. Callers kick\n * this off early and await it only at commit time.\n */\nexport async function fetchSyncActor(\n api: ApiClient,\n timeoutMs = 5_000,\n): Promise<SyncActor | null> {\n // Served from the profile cache after the first sync; /api/me only\n // fires on a miss.\n return resolveGitSyncActor(() => requestSyncActor(api, timeoutMs));\n}\n\nasync function requestSyncActor(\n api: ApiClient,\n timeoutMs: number,\n): Promise<SyncActor | null> {\n try {\n const raw = await Promise.race([\n api.get<Record<string, unknown>>(\"/api/me\"),\n new Promise<never>((_, reject) => {\n const timer = setTimeout(\n () => reject(new Error(`timed out after ${timeoutMs}ms`)),\n timeoutMs,\n );\n // Don't keep the process alive just for the stamp timeout.\n timer.unref?.();\n }),\n ]);\n return actorFromMe(raw);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n console.warn(\n ` (skipping user-stamp on sync commit — couldn't fetch /api/me: ${reason})`,\n );\n return null;\n }\n}\n","import chalk from \"chalk\";\nimport { Command } from \"commander\";\nimport ora from \"ora\";\nimport prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { readThemeConfig, writeThemeConfig } from \"../theme-config.js\";\nimport { ThemeAssetManifest } from \"../theme/asset-manifest.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { PushConflictError, Syncer } from \"../theme/syncer.js\";\nimport { ShadowRepo } from \"../theme/shadow-repo.js\";\nimport {\n commitPushedState,\n conflictMarkerBlockLine,\n diffAgainstShadow,\n finalizeManifestOnlyPush,\n findUnresolvedConflicts,\n} from \"../theme/merge-push.js\";\nimport { seedBaselineFromServer } from \"../theme/auto-baseline.js\";\nimport { migrateLegacyChecksumsIntoShadow } from \"../theme/legacy-migration.js\";\nimport {\n fetchSyncActor,\n syncCommitSubject,\n type SyncActor,\n} from \"../theme/sync-identity.js\";\nimport type { ChangeEntry } from \"@fluid-app/fluid-cli\";\nimport { themes } from \"@fluid-app/themes-api-client\";\nimport {\n selectTheme,\n findTheme,\n type ApplicationTheme,\n} from \"../theme-picker.js\";\nimport { findWorkspace, resolveThemeRootFromCwd } from \"../workspace.js\";\nimport { formatError } from \"../theme/format-error.js\";\nimport { findLiquidBlockTagDiagnostics } from \"../theme/liquid-delimiters.js\";\n\nexport function createPushCommand(): Command {\n return new Command(\"push\")\n .description(\"Push local theme files to a remote theme\")\n .option(\"-t, --theme <name-or-id>\", \"Theme name or ID to push to\")\n .option(\"-n, --nodelete\", \"Do not delete remote files missing locally\")\n .option(\n \"-f, --force\",\n \"Skip local Liquid validation and the server-side merge check\",\n )\n .option(\"-p, --publish\", \"Publish the theme after pushing\")\n .option(\n \"-u, --unpublished\",\n \"Create a new unpublished theme and push to it\",\n )\n .option(\n \"--auto-baseline\",\n \"When no local baseline exists, record the server's current state \" +\n \"as the baseline and push only what differs locally (never \" +\n \"modifies local files, never deletes server-only files)\",\n )\n .option(\"--root <path>\", \"Theme root directory\", \".\")\n .action(\n async (opts: {\n theme?: string;\n nodelete?: boolean;\n force?: boolean;\n publish?: boolean;\n unpublished?: boolean;\n autoBaseline?: boolean;\n root: string;\n }) => {\n requireToken();\n\n let rootPath = opts.root;\n if (rootPath === \".\") {\n const workspace = findWorkspace();\n if (workspace) {\n rootPath = resolveThemeRootFromCwd(workspace) ?? rootPath;\n }\n }\n\n const themeRoot = new ThemeRoot(rootPath);\n if (!themeRoot.isValid()) {\n console.error(`'${rootPath}' does not look like a theme directory.`);\n process.exit(1);\n }\n\n const api = createApiClient();\n const config = readThemeConfig(themeRoot.root);\n let theme: ApplicationTheme;\n\n if (opts.unpublished) {\n const { name } = await prompts(\n {\n type: \"text\",\n name: \"name\",\n message: \"Name for the new theme\",\n },\n { onCancel: () => process.exit(130) },\n );\n if (!name) {\n console.error(\"Theme name is required.\");\n process.exit(1);\n }\n const body = await themes.createApplicationTheme(api, {\n application_theme: { name, status: \"draft\" },\n });\n theme = body.application_theme;\n console.log(\n `Created unpublished theme: ${theme.name} (#${theme.id})`,\n );\n } else if (opts.theme) {\n theme = await findTheme(api, opts.theme);\n } else if (config) {\n console.log(\n ` Using theme from .fluid-theme.json: ${chalk.bold(config.themeName)} (#${config.themeId})`,\n );\n const body = await themes.getApplicationTheme(api, config.themeId);\n theme = body.application_theme;\n } else {\n theme = await selectTheme(api, \"Select a theme to push to\");\n }\n\n const shadow = await ShadowRepo.open(themeRoot.root, theme.id);\n\n // Legacy checksum-era dirs (pre-shadow CLI) migrate here — on\n // push as well as pull, so a dir that never pulled with the\n // new CLI still recovers its merge base instead of dead-ending\n // on the baseline check below. Idempotent; the migration wins\n // over `--auto-baseline` server seeding because a migrated\n // HEAD holds the local \"unmodified since last sync\" blobs.\n await migrateLegacyChecksumsIntoShadow({\n shadow,\n themeRoot,\n absoluteRoot: themeRoot.root,\n themeId: theme.id,\n });\n\n const localFiles = themeRoot.files().filter((f) => f.exists);\n\n // Lazy, memoized identity lookup for commit stamping — only\n // fired when we actually commit, never blocks or fails a push.\n let actorPromise: Promise<SyncActor | null> | null = null;\n const getActor = (): Promise<SyncActor | null> =>\n (actorPromise ??= fetchSyncActor(api));\n\n // Refuse to push files that still contain `<<<<<<< / >>>>>>>`\n // markers from a previous merge — the whole point of writing\n // them was to hand resolution to the user; sending them\n // upstream would ship broken content. This guard is absolute:\n // `--auto-baseline` (non-interactive) refuses too, but emits a\n // machine-readable line the desktop can surface well.\n const unresolved = findUnresolvedConflicts(localFiles);\n if (unresolved.length > 0) {\n console.log();\n console.log(\n chalk.red(\n `✗ ${unresolved.length} file(s) still contain unresolved conflict markers:`,\n ),\n );\n for (const key of unresolved) console.log(` ${key}`);\n console.log();\n console.log(\n ` Edit each file to reconcile the ${chalk.cyan(\"<<<<<<<\")} / ${chalk.cyan(\">>>>>>>\")} sections,`,\n );\n console.log(` then re-run ${chalk.cyan(\"fluid theme push\")}.`);\n console.log();\n if (opts.autoBaseline) {\n console.error(conflictMarkerBlockLine(unresolved));\n }\n process.exit(1);\n }\n\n const syncer = new Syncer(api, theme.id, themeRoot);\n\n // baseSha — server-side merge check on every write. Skipped\n // when `--force`, when the config was written by an older CLI\n // that didn't yet capture the pull's `content_version_sha`,\n // or when we're pushing to a different theme than the one the\n // config was written for (a `--unpublished` create, or\n // `--theme` pointing somewhere else). `baseSha` is per-theme —\n // reusing one theme's sha as the base of another theme's push\n // always 409s.\n const configMatchesTheme = config?.themeId === theme.id;\n let baseSha =\n opts.force || !configMatchesTheme ? null : (config?.baseSha ?? null);\n\n // A baseline exists when either half is present: shadow HEAD\n // (the local diff base — written by pull, a legacy migration,\n // or a previous auto-baseline) or a stored `baseSha` (the\n // server-side race check). HEAD alone is enough to diff\n // honestly; a missing baseSha only skips the server preflight.\n const hasBaseline = baseSha != null || (await shadow.hasHead());\n\n // No baseline at all: without `--auto-baseline`, refuse —\n // otherwise the CLI silently clobbers whatever the server\n // currently has, with no way for the user to see the\n // divergence first. `--force` opts out (explicit clobber);\n // `--unpublished` skips because the theme was created empty\n // in this same command and has nothing to protect.\n if (!opts.force && !opts.unpublished && !hasBaseline) {\n if (!opts.autoBaseline) {\n console.error();\n console.error(\n chalk.red(\n `No local baseline for theme \"${theme.name}\" (#${theme.id}).`,\n ),\n );\n console.error();\n console.error(\n ` Run ${chalk.cyan(`fluid theme pull -t ${theme.id}`)} first to sync down the current server state,`,\n );\n console.error(\n ` then push — this way you see what would change before it goes live.`,\n );\n console.error();\n console.error(\n ` Or re-run with ${chalk.cyan(\"--auto-baseline\")} to record the server's current state`,\n );\n console.error(\n ` as the baseline and push only what differs locally (local files are never modified).`,\n );\n console.error();\n console.error(\n ` Or, if you know what you're doing and want to overwrite the server's current`,\n );\n console.error(\n ` contents wholesale, re-run with ${chalk.cyan(\"--force\")}.`,\n );\n console.error();\n process.exit(1);\n }\n\n // `--auto-baseline`: record the server's current state as\n // the baseline WITHOUT touching a single local file, then\n // fall through to the normal diff. Server-only files are\n // deliberately kept out of the baseline so they can never\n // be reported as local deletions (see auto-baseline.ts).\n const baselineSpinner = ora(\n `No local baseline — recording current server state for ${theme.name} (#${theme.id})…`,\n ).start();\n try {\n const resources = await syncer.downloadAll();\n const seedResult = await seedBaselineFromServer({\n shadow,\n themeRoot,\n remote: resources,\n fetchBinary: (url) => syncer.downloadBinaryAsset(url),\n message: syncCommitSubject(\n \"Baseline from server\",\n await getActor(),\n ),\n });\n baseSha = syncer.remoteSha() ?? null;\n const parts = [`recorded ${seedResult.recorded.length} file(s)`];\n if (seedResult.serverOnly.length > 0) {\n parts.push(\n `left ${seedResult.serverOnly.length} server-only file(s) untouched`,\n );\n }\n baselineSpinner.succeed(`Baseline recorded — ${parts.join(\", \")}.`);\n for (const err of seedResult.errors) {\n console.warn(` ${chalk.yellow(\"warn\")} ${err}`);\n }\n } catch (e) {\n baselineSpinner.fail(\n `Could not record a baseline from the server: ${formatError(e)}`,\n );\n process.exit(1);\n }\n }\n\n // Diff against shadow HEAD is the source of truth for \"what\n // changed since last sync\" — replaces the sha256 checksum map.\n const { changed, deleted } = await diffAgainstShadow(themeRoot, shadow);\n const managedAssetCount = new ThemeAssetManifest(themeRoot.root).keys()\n .length;\n\n if (\n changed.length === 0 &&\n deleted.length === 0 &&\n managedAssetCount === 0\n ) {\n console.log(\"Nothing to push — local matches the last synced state.\");\n await persistConfig();\n return;\n }\n\n const spinner = ora(`Pushing to ${theme.name} (#${theme.id})…`).start();\n\n // Validate before linking URL references so a schema failure cannot\n // mutate the target theme at all.\n if (!opts.force) {\n const validationErrors: string[] = [];\n for (const file of changed) {\n if (!file.isLiquid) continue;\n for (const diagnostic of file.validateSchema()) {\n if (diagnostic.severity === \"error\") {\n validationErrors.push(\n `${file.relativePath}: ${diagnostic.message}`,\n );\n }\n }\n for (const diagnostic of findLiquidBlockTagDiagnostics(\n file.read(),\n )) {\n validationErrors.push(\n `${file.relativePath}: ${diagnostic.message}`,\n );\n }\n }\n if (validationErrors.length > 0) {\n spinner.fail(\n `Liquid validation failed (${validationErrors.length} error(s)). Use --force to skip.`,\n );\n for (const error of validationErrors) console.error(` ${error}`);\n process.exit(1);\n }\n }\n try {\n await syncer.preflightPush(baseSha);\n baseSha = syncer.remoteSha() ?? baseSha;\n } catch (e) {\n if (e instanceof PushConflictError) {\n renderPullFirst(spinner);\n process.exit(1);\n }\n throw e;\n }\n\n let linked = 0;\n try {\n linked = await syncer.linkManagedAssets({ replace: true });\n // Reference writes are outside the ordinary resource PUT path. The\n // syncer refreshes its remote SHA afterwards so following PUTs keep\n // the existing Phase-003a conflict chain intact.\n baseSha = syncer.remoteSha() ?? baseSha;\n } catch (error) {\n spinner.fail(\n `Could not save remote asset references: ${formatError(error)}`,\n );\n process.exit(1);\n }\n\n if (changed.length === 0 && deleted.length === 0 && linked === 0) {\n try {\n await finalizeManifestOnlyPush(syncer, themeRoot, shadow);\n } catch (error) {\n spinner.fail(\n `Could not finalize remote asset references: ${formatError(error)}`,\n );\n process.exit(1);\n }\n spinner.succeed(\"Nothing to push — local matches the remote theme.\");\n await persistConfig();\n return;\n }\n\n let uploaded = 0;\n let deletedCount = 0;\n const errors: string[] = [];\n let progress = 0;\n const total = changed.length + (opts.nodelete ? 0 : deleted.length);\n\n for (const file of changed) {\n try {\n await syncer.uploadFile(file, baseSha);\n baseSha = syncer.remoteSha() ?? baseSha;\n uploaded++;\n } catch (e) {\n if (e instanceof PushConflictError) {\n spinner.stop();\n renderPullFirst(ora());\n process.exit(1);\n }\n errors.push(`Upload ${file.relativePath}: ${formatError(e)}`);\n }\n spinner.text = `Pushing ${++progress}/${total} files…`;\n }\n\n if (!opts.nodelete) {\n for (const key of deleted) {\n try {\n await syncer.deleteRemoteFile(key, baseSha);\n baseSha = syncer.remoteSha() ?? baseSha;\n deletedCount++;\n } catch (e) {\n if (e instanceof PushConflictError) {\n spinner.stop();\n renderPullFirst(ora());\n process.exit(1);\n }\n errors.push(`Delete ${key}: ${formatError(e)}`);\n }\n spinner.text = `Pushing ${++progress}/${total} files…`;\n }\n }\n\n if (errors.length === 0) {\n try {\n // The selected target now owns every manifest reference, so a\n // later deleted dev sandbox cannot force a byte upload again.\n syncer.repointManagedAssetsToCurrentTheme();\n } catch (error) {\n errors.push(`Save asset provenance: ${formatError(error)}`);\n }\n }\n\n // Every write is on the server; ask Fluid to commit them as one\n // version. The writes themselves only mark the theme changed — this\n // one call is what makes a 120-file push a single commit instead of\n // 120, and it is the reason the server no longer has to guess where\n // an operation ended.\n //\n // Sent even when some files failed: what did land is real, and Fluid\n // commits whatever state it now holds. Skipping it would leave that\n // work uncommitted until a sweep. Managed-asset reference writes are\n // server writes too, so a link-only push also commits.\n if (uploaded > 0 || deletedCount > 0 || linked > 0) {\n await syncer.requestSync();\n }\n\n if (errors.length) {\n spinner.warn(`Pushed with ${errors.length} error(s).`);\n for (const err of errors) console.error(` ${err}`);\n // The live theme is missing every rejected file, so this is a\n // failure. Mist's lifecycle retry runs `fluid theme push\n // --auto-baseline` and keys off the exit code, and every other\n // failure in this command already exits 1 — exiting 0 here\n // reported a publish that did not happen.\n //\n // exitCode rather than process.exit(): the command still has\n // output to flush, and an immediate exit can truncate it.\n process.exitCode = 1;\n } else {\n spinner.succeed(\n `Pushed ${uploaded} file(s), saved ${linked} remote asset reference(s)` +\n (deletedCount > 0\n ? `, deleted ${deletedCount} remote file(s).`\n : \".\"),\n );\n }\n\n // Roll shadow HEAD forward only when every write succeeded —\n // committing the working tree on a partial push would make\n // `diffAgainstShadow` treat the failed files as \"already\n // synced\" on the next push, silently swallowing the retry.\n // Leave HEAD stale on failure and let the next push re-diff\n // and re-attempt.\n if (errors.length === 0) {\n // Describe what the push carried rather than stamping every\n // entry \"Push from Fluid CLI\". `changed` is the uploaded set\n // and `deleted` the removed keys — exactly what landed, since\n // we only get here when no write failed.\n const pushedChanges: ChangeEntry[] = [\n ...changed.map((file) => ({\n status: \"modified\" as const,\n path: file.relativePath,\n })),\n ...(opts.nodelete\n ? []\n : deleted.map((key) => ({\n status: \"deleted\" as const,\n path: key,\n }))),\n ];\n await commitPushedState(\n themeRoot,\n shadow,\n syncCommitSubject(\n \"Push\",\n await getActor(),\n new Date(),\n pushedChanges,\n ),\n );\n }\n\n await persistConfig();\n\n /**\n * Persist `.fluid-theme.json`. When a config already exists,\n * this is the pre-existing baseSha refresh. When it doesn't\n * (a scaffold's first `--auto-baseline` push), bind the dir\n * to the theme now — best-effort, since the company subdomain\n * needs one more API call — so the next push/pull needs no\n * interactive picker.\n */\n async function persistConfig(): Promise<void> {\n if (config) {\n writeThemeConfig(themeRoot.root, {\n themeId: theme.id,\n themeName: theme.name,\n company: config.company,\n baseSha: baseSha ?? undefined,\n assetManifestSha: new ThemeAssetManifest(\n themeRoot.root,\n ).fingerprint({ excludePending: true }),\n });\n return;\n }\n if (!opts.autoBaseline) return;\n try {\n const res = await api.get<{\n data?: { company?: { subdomain?: string } };\n }>(\"/api/company/v1/companies/me\");\n const subdomain = res.data?.company?.subdomain;\n if (!subdomain) return;\n writeThemeConfig(themeRoot.root, {\n themeId: theme.id,\n themeName: theme.name,\n company: subdomain,\n baseSha: baseSha ?? undefined,\n assetManifestSha: new ThemeAssetManifest(\n themeRoot.root,\n ).fingerprint({ excludePending: true }),\n });\n } catch {\n // Best-effort — the shadow baseline alone is enough for\n // the next push to diff correctly.\n }\n }\n\n if (opts.publish) {\n const pubSpinner = ora(\"Publishing theme…\").start();\n try {\n await themes.publishApplicationTheme(api, theme.id);\n pubSpinner.succeed(\"Theme published.\");\n } catch (e) {\n pubSpinner.fail(`Publish failed: ${e}`);\n // Content uploaded but the theme is still a draft. Without\n // this the caller is told the publish succeeded.\n process.exitCode = 1;\n }\n }\n },\n );\n}\n\nfunction renderPullFirst(spinner: ReturnType<typeof ora>): void {\n spinner.fail(\"Server has changed since your last pull. Push aborted.\");\n console.log();\n console.log(\n ` ${chalk.cyan(\"Run `fluid theme pull` first\")} to merge the remote changes,`,\n );\n console.log(\n ` then re-run push. Use ${chalk.cyan(\"fluid theme push --force\")} to overwrite anyway.`,\n );\n console.log();\n}\n","import { existsSync, readFileSync, readdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { readThemeConfig } from \"../theme-config.js\";\nimport { ThemeAssetManifest } from \"./asset-manifest.js\";\nimport { diffAgainstShadow } from \"./merge-push.js\";\nimport { ThemeRoot } from \"./root.js\";\nimport { ShadowRepo } from \"./shadow-repo.js\";\n\nfunction containsSymlink(directory: string): boolean {\n return readdirSync(directory, { withFileTypes: true }).some((entry) => {\n if (entry.name.startsWith(\".\") || entry.name === \"node_modules\")\n return false;\n return (\n entry.isSymbolicLink() ||\n (entry.isDirectory() && containsSymlink(join(directory, entry.name)))\n );\n });\n}\n\n/** Fail closed for unattended updates. Desktop snapshots can advance shadow\n * HEAD with local work, so only a CLI sync baseline is eligible. */\nexport async function canPullThemeInBackground(\n root: string,\n themeId: number,\n): Promise<boolean> {\n try {\n if (containsSymlink(root)) return false;\n const config = readThemeConfig(root);\n if (\n !config ||\n config.themeId !== themeId ||\n !existsSync(join(root, \".fluid-theme\", \"repo\", \"HEAD\"))\n )\n return false;\n const storedId = readFileSync(\n join(root, \".fluid-theme\", \"theme-id\"),\n \"utf8\",\n ).trim();\n if (storedId !== String(themeId)) return false;\n const manifestPath = join(root, \".fluid-assets.json\");\n if (existsSync(manifestPath)) {\n const raw: unknown = JSON.parse(readFileSync(manifestPath, \"utf8\"));\n if (\n !raw ||\n typeof raw !== \"object\" ||\n !(\"version\" in raw) ||\n raw.version !== 1 ||\n !(\"assets\" in raw) ||\n !raw.assets ||\n typeof raw.assets !== \"object\" ||\n Array.isArray(raw.assets)\n )\n return false;\n const manifest = new ThemeAssetManifest(root);\n if (manifest.entries().some(([, link]) => link.pending)) return false;\n if (\n !config.assetManifestSha ||\n config.assetManifestSha !== manifest.fingerprint()\n )\n return false;\n } else if (\n config.assetManifestSha &&\n config.assetManifestSha !== new ThemeAssetManifest(root).fingerprint()\n )\n return false;\n const shadow = await ShadowRepo.open(root, themeId);\n if (!(await shadow.hasSyncedHead())) return false;\n const diff = await diffAgainstShadow(new ThemeRoot(root), shadow);\n return diff.changed.length === 0 && diff.deleted.length === 0;\n } catch {\n return false;\n }\n}\n","import {\n BackgroundPullChangedError,\n type BackgroundPullGuard,\n} from \"./background-pull-guard.js\";\nimport { unlinkSync } from \"node:fs\";\nimport { sep } from \"node:path\";\nimport type { components } from \"@fluid-app/themes-api-client\";\nimport {\n MANAGED_ASSET_SHADOW_SENTINEL,\n ThemeAssetManifest,\n} from \"./asset-manifest.js\";\nimport { ShadowRepo, looksBinary, readIfExists } from \"./shadow-repo.js\";\nimport type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport { isStylesheetKey } from \"./stylesheet-keys.js\";\nimport { commitPushedState } from \"./merge-push.js\";\nimport { syncCommitSubject, type SyncActor } from \"./sync-identity.js\";\n\ntype RemoteResource = components[\"schemas\"][\"ApplicationThemeResource\"];\n\n/** `write(..., RESOLVES_CONFLICT)`: this write is the chosen side of a conflict. */\nconst RESOLVES_CONFLICT = true;\n\n/**\n * Appended to every per-file write failure. Resource keys are verbatim\n * relative paths, so a template named `Sale 11/14/2025, 12:30 PM` asks\n * the filesystem for directories that Windows (and any path already\n * occupied by a file) refuses. Renaming the template in the admin is\n * the only fix the user owns.\n */\nconst RENAME_REMEDY =\n 'Rename the template in the admin visual builder to remove / \\\\ : * ? \" < > | from its name, then pull again.';\n\nexport interface MergePullResult {\n /** Files written from the remote without any local content to merge. */\n written: number;\n /** Files that went through a clean three-way merge with no markers. */\n merged: number;\n /** Files where markers remain — the caller must surface these. */\n conflicts: string[];\n /** Conflicting files auto-resolved to one side via `resolve`. */\n autoResolved: string[];\n /** Files removed locally because remote no longer emitted them. */\n deleted: number;\n /** Files that needed no change (already in sync). */\n skipped: number;\n /** Non-fatal per-file errors. */\n errors: string[];\n}\n\nexport type FetchBinary = (url: string) => Promise<Buffer>;\n\nexport interface MergePullInput {\n themeRoot: ThemeRoot;\n backgroundGuard?: BackgroundPullGuard;\n shadow: ShadowRepo;\n remote: RemoteResource[];\n fetchBinary: FetchBinary;\n delete: boolean;\n /**\n * When true, skip the merge and take remote wholesale for every\n * file. Used by `fluid theme pull --force` when the user wants a\n * clean slate. HEAD still advances to reflect the state we wrote.\n */\n force?: boolean;\n /**\n * Remote resources already represented by `.fluid-assets.json`. They are\n * intentionally not materialized; ShadowRepo retains only a tiny path\n * sentinel, never their binary bytes.\n */\n skipRemoteKeys?: ReadonlySet<string>;\n /**\n * Non-interactive conflict resolution (`fluid theme pull --resolve\n * <local|remote>`): conflicting hunks are resolved to the chosen\n * side via `git merge-file --ours/--theirs` instead of writing\n * `<<<<<<<` markers; binary conflicts keep the whole chosen side.\n * Because \"remote\" resolution discards local hunks that live\n * NOWHERE else (shadow HEAD only holds the last-synced base, not\n * the user's edits), the pre-merge working tree is committed to the\n * shadow repo first — so the discarded state stays restorable.\n * Undefined = current behavior (markers).\n */\n resolve?: \"local\" | \"remote\";\n /**\n * Signed-in user for commit stamping (best-effort — null falls back\n * to the legacy machine-flavored subjects).\n */\n actor?: SyncActor | null;\n onProgress?: (done: number, total: number) => void;\n}\n\n/**\n * Reconcile the just-downloaded remote tree against the working tree,\n * using the shadow repo's HEAD as the merge base. Text files that\n * diverge on both sides get run through `git merge-file`; the result\n * — clean or with `<<<<<<<` markers — is written to disk. Binary\n * files fall back to \"take remote\" because there is no principled\n * three-way merge for them.\n *\n * HEAD advances to reflect the remote state we just materialized,\n * even when unresolved conflict markers remain in the working tree.\n * The user resolves markers in their editor and re-runs push; push\n * refuses to send files whose content still starts with a marker,\n * so a half-resolved push cannot silently ship broken content.\n */\nexport async function mergePull(\n input: MergePullInput,\n): Promise<MergePullResult> {\n const { themeRoot, shadow, remote, fetchBinary, onProgress } = input;\n const doDelete = input.delete;\n const result: MergePullResult = {\n written: 0,\n merged: 0,\n conflicts: [],\n autoResolved: [],\n deleted: 0,\n skipped: 0,\n errors: [],\n };\n\n // In `resolve` mode, working-tree writes are DEFERRED until after\n // the merge loop: if any conflict got auto-resolved, the pre-merge\n // working tree (still untouched at that point) is committed to the\n // shadow repo first, so the side we discard remains restorable.\n const pendingWrites: Array<{\n key: string;\n file: ThemeFile;\n content: Buffer;\n record: () => void;\n resolvesConflict: boolean;\n }> = [];\n // Keys whose bytes never reached disk — refused by the traversal\n // guard, or failed on the write. They must be kept out of the shadow\n // HEAD commit below, exactly like a failed download: HEAD is what\n // push diffs against, so recording content we did not write would\n // turn a tolerated failure into a remote DELETE. Refused keys have a\n // second reason — git rejects paths like `../x` outright, and the\n // commit takes the whole pull down with it.\n const unwrittenKeys = new Set<string>();\n const tryWrite = (key: string, file: ThemeFile, content: Buffer): boolean => {\n try {\n if (input.backgroundGuard)\n input.backgroundGuard.mutate([key], () => file.write(content));\n else file.write(content);\n return true;\n } catch (e) {\n if (e instanceof BackgroundPullChangedError) throw e;\n unwrittenKeys.add(key);\n result.errors.push(`Reconcile ${key}: ${errMsg(e)}. ${RENAME_REMEDY}`);\n return false;\n }\n };\n /**\n * Write `content`, then apply `record` — the bookkeeping that says\n * which bucket of the summary this file counted as. The two travel\n * together, including into the deferred flush: bookkeeping applied\n * when the write was merely QUEUED would report a file the flush\n * never managed to write as both written and errored.\n */\n const write = (\n key: string,\n file: ThemeFile,\n content: Buffer,\n record: () => void,\n resolvesConflict = false,\n ): void => {\n if (input.resolve) {\n pendingWrites.push({ key, file, content, record, resolvesConflict });\n return;\n }\n if (tryWrite(key, file, content)) record();\n };\n\n const remoteContent = new Map<string, Buffer>();\n const remoteKeys = new Set<string>();\n let done = 0;\n for (const resource of remote) {\n remoteKeys.add(resource.key);\n if (input.skipRemoteKeys?.has(resource.key)) {\n onProgress?.(++done, remote.length);\n continue;\n }\n try {\n const buf = await materialize(resource, fetchBinary);\n if (buf) remoteContent.set(resource.key, buf);\n } catch (e) {\n result.errors.push(`Download ${resource.key}: ${errMsg(e)}`);\n }\n onProgress?.(++done, remote.length);\n }\n\n input.backgroundGuard?.assertUnchanged();\n for (const [key, remoteBuf] of remoteContent) {\n const file = themeRoot.file(key);\n\n if (!file.absolutePath.startsWith(themeRoot.root + sep)) {\n result.errors.push(`Reconcile ${key}: path traversal detected`);\n unwrittenKeys.add(key);\n continue;\n }\n\n if (input.force) {\n // `--force`: clobber everything with the remote content. HEAD\n // still advances below so the next pull's merge base is right.\n write(key, file, remoteBuf, () => result.written++);\n continue;\n }\n\n const localBuf = readIfExists(file.absolutePath);\n const baseBuf = await shadow.blobAtHead(key);\n\n if (localBuf == null) {\n write(key, file, remoteBuf, () => result.written++);\n continue;\n }\n\n if (localBuf.equals(remoteBuf)) {\n result.skipped++;\n continue;\n }\n\n if (baseBuf && localBuf.equals(baseBuf)) {\n write(key, file, remoteBuf, () => result.written++);\n continue;\n }\n\n if (baseBuf && remoteBuf.equals(baseBuf)) {\n // A prior unresolved pull advances shadow HEAD to the clean remote\n // state while leaving markers in the working tree. A later\n // `--resolve remote` must be able to finish that interrupted clone;\n // treating the marker-filled file as an ordinary local edit traps\n // every retry in the same broken state.\n if (\n input.resolve === \"remote\" &&\n !looksBinary(localBuf) &&\n hasGeneratedConflictMarkers(localBuf)\n ) {\n write(\n key,\n file,\n remoteBuf,\n () => result.autoResolved.push(`${key} (cleared conflict markers)`),\n RESOLVES_CONFLICT,\n );\n continue;\n }\n // Server didn't move; keep the user's local edits.\n result.skipped++;\n continue;\n }\n\n const isBinary =\n looksBinary(localBuf) ||\n looksBinary(remoteBuf) ||\n (baseBuf ? looksBinary(baseBuf) : false);\n\n if (isBinary) {\n // No textual merge for binary files: whole-file either/or.\n if (input.resolve === \"local\") {\n // Keep the local side untouched — nothing to write.\n result.autoResolved.push(`${key} (binary — kept local)`);\n continue;\n }\n // Take remote. In `--resolve remote` that's the chosen side;\n // otherwise flag it so the user can compare against the shadow\n // repo (`git show HEAD:...`) and reapply local intent by hand.\n write(\n key,\n file,\n remoteBuf,\n () => {\n if (input.resolve === \"remote\") {\n result.autoResolved.push(`${key} (binary — kept remote)`);\n } else {\n result.conflicts.push(`${key} (binary — kept remote)`);\n }\n },\n input.resolve === \"remote\",\n );\n continue;\n }\n\n // First merge WITHOUT a favor side so we know whether this file\n // actually conflicts — that drives honest bookkeeping (merged vs\n // autoResolved) and the pre-merge snapshot decision.\n const { merged, hasConflicts } = await shadow.merge3(\n baseBuf,\n localBuf,\n remoteBuf,\n );\n if (hasConflicts && input.resolve) {\n const favored = await shadow.merge3(\n baseBuf,\n localBuf,\n remoteBuf,\n input.resolve,\n );\n write(\n key,\n file,\n favored.merged,\n () => result.autoResolved.push(key),\n RESOLVES_CONFLICT,\n );\n continue;\n }\n write(key, file, merged, () => {\n if (hasConflicts) result.conflicts.push(key);\n else result.merged++;\n });\n }\n\n // Flush deferred writes (resolve mode only). When any conflict was\n // auto-resolved, snapshot the pre-merge working tree first — it is\n // the ONLY place the losing side of each resolution still exists.\n if (input.resolve) {\n if (pendingWrites.some((w) => w.resolvesConflict)) {\n await commitPushedState(\n themeRoot,\n shadow,\n syncCommitSubject(\"Snapshot before pull\", input.actor ?? null),\n );\n }\n input.backgroundGuard?.assertUnchanged();\n for (const { key, file, content, record } of pendingWrites) {\n if (tryWrite(key, file, content)) record();\n }\n }\n\n if (doDelete && (await shadow.hasHead())) {\n for (const file of themeRoot.files()) {\n if (remoteKeys.has(file.relativePath)) continue;\n // Stylesheet-hidden files are intentionally absent from the\n // resources index — deleting them locally would fight the\n // STYLESHEET_STRICT_INPUT feature, not honor a real deletion.\n if (isStylesheetKey(file.relativePath)) continue;\n\n const baseBuf = await shadow.blobAtHead(file.relativePath);\n if (!baseBuf) continue; // never seen by shadow — leave alone\n const localBuf = readIfExists(file.absolutePath);\n if (!localBuf) continue;\n if (!localBuf.equals(baseBuf)) continue; // user has local edits\n\n try {\n if (input.backgroundGuard)\n input.backgroundGuard.mutate([file.relativePath], () =>\n unlinkSync(file.absolutePath),\n );\n else unlinkSync(file.absolutePath);\n result.deleted++;\n } catch (error) {\n if (error instanceof BackgroundPullChangedError) throw error;\n // ignore best-effort deletion\n }\n }\n }\n\n // Advance HEAD to the remote state we just downloaded. Even when\n // some files carry unresolved conflict markers, HEAD reflects the\n // server's clean bytes — the next push's diff-against-HEAD is what\n // surfaces the markers as \"unresolved\".\n //\n // Preserve failed-download paths from the previous HEAD so a\n // transient network error doesn't quietly drop them from the merge\n // base. If a file was in HEAD before the pull and its download\n // just failed, the next pull needs the previous blob as the base\n // for its merge — otherwise `blobAtHead` returns null next time and\n // a server-side change fires a spurious null-base conflict on a\n // file the user never touched.\n //\n // Keys whose write failed take the same route: their bytes are on\n // the server but not on disk, so recording the remote content here\n // would claim a state this checkout never had.\n const commitEntries: Array<{ path: string; sha: string }> = [];\n for (const [key, buf] of remoteContent) {\n if (unwrittenKeys.has(key)) continue;\n commitEntries.push({ path: key, sha: await shadow.writeBlob(buf) });\n }\n for (const key of remoteKeys) {\n if (input.skipRemoteKeys?.has(key)) continue;\n // Downloaded AND written — already added above.\n if (remoteContent.has(key) && !unwrittenKeys.has(key)) continue;\n const prevBlob = await shadow.blobAtHead(key);\n if (prevBlob == null) continue; // wasn't in HEAD to begin with\n commitEntries.push({ path: key, sha: await shadow.writeBlob(prevBlob) });\n }\n const assetManifest = new ThemeAssetManifest(themeRoot.root);\n let managedAssetSentinelSha: string | undefined;\n for (const key of input.skipRemoteKeys ?? []) {\n // Ignored resources without a manifest link remain outside the shadow\n // entirely. Otherwise removing an ignore rule could make a skipped file\n // look like a local deletion on the next push.\n if (!remoteKeys.has(key) || !assetManifest.has(key)) continue;\n managedAssetSentinelSha ??= await shadow.writeBlob(\n MANAGED_ASSET_SHADOW_SENTINEL,\n );\n commitEntries.push({\n path: key,\n sha: managedAssetSentinelSha,\n });\n }\n // A pull containing only manifest-backed assets still needs a shadow commit:\n // sentinel entries replace any legacy binary blobs while keeping a baseline\n // for an intentional manifest-entry removal on a later push.\n // When the server becomes empty, record an empty tree rather than retaining\n // stale paths from HEAD as a future merge/delete baseline — but only when\n // the server really is empty. Empty entries with a non-empty remote mean\n // every write failed, and HEAD must survive as the next pull's merge base.\n if (\n commitEntries.length > 0 ||\n (remoteKeys.size === 0 && (await shadow.hasHead()))\n ) {\n input.backgroundGuard?.assertUnchanged();\n await shadow.commitState(\n commitEntries,\n syncCommitSubject(\"Pull\", input.actor ?? null),\n );\n }\n\n return result;\n}\n\nfunction hasGeneratedConflictMarkers(content: Buffer): boolean {\n const text = content.toString(\"utf8\");\n return (\n /^<<<<<<< local\\r?$/m.test(text) &&\n /^=======\\r?$/m.test(text) &&\n /^>>>>>>> remote\\r?$/m.test(text)\n );\n}\n\nasync function materialize(\n resource: RemoteResource,\n fetchBinary: FetchBinary,\n): Promise<Buffer | null> {\n if (resource.resource_type === \"FileResource\" && resource.url) {\n return fetchBinary(resource.url);\n }\n if (resource.content == null) return null;\n const text =\n typeof resource.content === \"string\"\n ? resource.content\n : JSON.stringify(resource.content);\n return Buffer.from(text);\n}\n\nfunction errMsg(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n","import {\n BackgroundPullChangedError,\n BackgroundPullGuard,\n} from \"../theme/background-pull-guard.js\";\nimport { canPullThemeInBackground } from \"../theme/background-pull.js\";\nimport { join, resolve } from \"node:path\";\nimport chalk from \"chalk\";\nimport { Command } from \"commander\";\nimport ora from \"ora\";\nimport prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { readThemeConfig, writeThemeConfig } from \"../theme-config.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { Syncer } from \"../theme/syncer.js\";\nimport { ShadowRepo } from \"../theme/shadow-repo.js\";\nimport { mergePull } from \"../theme/merge-pull.js\";\nimport { migrateLegacyChecksumsIntoShadow } from \"../theme/legacy-migration.js\";\nimport { fetchSyncActor } from \"../theme/sync-identity.js\";\nimport { selectTheme, findTheme } from \"../theme-picker.js\";\nimport { findWorkspace, resolveThemeRootFromCwd } from \"../workspace.js\";\nimport { ThemeAssetManifest } from \"../theme/asset-manifest.js\";\nimport { CaseCollisionError } from \"../theme/case-collisions.js\";\n\ninterface CompanyMe {\n data: { company: { subdomain?: string; name?: string } };\n}\n\nasync function fetchCompanySubdomain(\n api: ReturnType<typeof createApiClient>,\n): Promise<string> {\n const res = await api.get<CompanyMe>(\"/api/company/v1/companies/me\");\n const subdomain = res.data?.company?.subdomain;\n if (!subdomain) {\n console.error(\n \"Could not determine company subdomain. Make sure your token is valid.\",\n );\n process.exit(1);\n }\n return subdomain;\n}\n\nexport function createPullCommand(): Command {\n return new Command(\"pull\")\n .description(\"Pull a remote theme to your local directory\")\n .option(\"-t, --theme <name-or-id>\", \"Theme name or ID to pull\")\n .option(\"-n, --nodelete\", \"Do not delete local files missing on remote\")\n .option(\"--root <path>\", \"Theme root directory\")\n .option(\"-y, --yes\", \"Skip confirmation prompt\")\n .option(\n \"--only-if-clean\",\n \"Skip with exit code 3 when local work cannot be proven synced\",\n )\n .option(\n \"-f, --force\",\n \"Overwrite local without merging (skip conflict markers)\",\n )\n .option(\n \"--resolve <side>\",\n \"Auto-resolve merge conflicts to one side instead of writing \" +\n \"conflict markers: 'local' keeps your files' hunks, 'remote' \" +\n \"takes the server's (for non-interactive use)\",\n )\n .action(\n async (opts: {\n theme?: string;\n nodelete?: boolean;\n root?: string;\n yes?: boolean;\n force?: boolean;\n resolve?: string;\n onlyIfClean?: boolean;\n }) => {\n let spinner: ReturnType<typeof ora> | undefined;\n try {\n requireToken();\n\n if (\n opts.resolve !== undefined &&\n opts.resolve !== \"local\" &&\n opts.resolve !== \"remote\"\n ) {\n console.error(\n `Invalid --resolve value \"${opts.resolve}\" — use \"local\" or \"remote\".`,\n );\n process.exit(1);\n }\n const resolveSide = opts.resolve as \"local\" | \"remote\" | undefined;\n\n const api = createApiClient();\n const workspace = findWorkspace();\n\n const theme = opts.theme\n ? await findTheme(api, opts.theme)\n : await selectTheme(api, \"Select a theme to pull\");\n\n const subdomain = await fetchCompanySubdomain(api);\n let root: string;\n if (opts.root) {\n root = opts.root;\n } else if (workspace) {\n root =\n resolveThemeRootFromCwd(workspace) ??\n join(workspace.root, \"local\", subdomain);\n } else {\n root = `.`;\n }\n\n const absoluteRoot = resolve(root);\n const existingConfig = readThemeConfig(absoluteRoot);\n const backgroundGuard = opts.onlyIfClean\n ? new BackgroundPullGuard(absoluteRoot)\n : undefined;\n\n console.log();\n console.log(` Theme: ${chalk.bold(theme.name)} (#${theme.id})`);\n console.log(` Company: ${chalk.bold(subdomain)}`);\n console.log(` Target: ${chalk.bold(absoluteRoot)}`);\n console.log();\n\n if (!opts.yes) {\n const { confirmed } = await prompts(\n {\n type: \"confirm\",\n name: \"confirmed\",\n message: \"Pull theme to this directory?\",\n initial: true,\n },\n { onCancel: () => process.exit(130) },\n );\n if (!confirmed) {\n console.log(\"Aborted.\");\n process.exit(0);\n }\n }\n\n if (\n opts.onlyIfClean &&\n !(await canPullThemeInBackground(absoluteRoot, theme.id))\n ) {\n console.log(\"Skipped: local changes or an unknown sync baseline.\");\n process.exit(3);\n }\n backgroundGuard?.assertUnchanged();\n const themeRoot = new ThemeRoot(root);\n const shadow = await ShadowRepo.open(absoluteRoot, theme.id);\n await migrateLegacyChecksumsIntoShadow({\n shadow,\n themeRoot,\n absoluteRoot,\n themeId: theme.id,\n });\n const syncer = new Syncer(api, theme.id, themeRoot);\n\n // Fire the identity lookup alongside the download — it never\n // rejects, and mergePull only awaits it at commit time.\n const actorPromise = fetchSyncActor(api);\n\n spinner = ora(`Pulling ${theme.name} (#${theme.id})…`).start();\n const resources = await syncer\n .downloadAll()\n .catch((error: unknown) => {\n if (!(error instanceof CaseCollisionError)) throw error;\n if (spinner) spinner.fail(error.message);\n process.exit(1);\n });\n // Recheck after network I/O and before any asset or content writes.\n if (\n opts.onlyIfClean &&\n !(await canPullThemeInBackground(absoluteRoot, theme.id))\n ) {\n spinner.stop();\n console.log(\"Skipped: local files changed while downloading.\");\n process.exit(3);\n }\n const externalizedAssets = await syncer.externalizePulledAssets(\n resources,\n {\n delete: !opts.nodelete,\n backgroundGuard,\n },\n );\n\n const result = await mergePull({\n themeRoot,\n shadow,\n backgroundGuard,\n remote: resources,\n fetchBinary: (url) => syncer.downloadBinaryAsset(url),\n delete: !opts.nodelete,\n force: opts.force ?? false,\n skipRemoteKeys: externalizedAssets.managedKeys,\n resolve: resolveSide,\n actor: await actorPromise,\n onProgress: (done, total) => {\n if (spinner) spinner.text = `Downloading ${done}/${total} files…`;\n },\n });\n result.errors.push(...externalizedAssets.errors);\n\n const parts: string[] = [];\n if (result.written > 0) parts.push(`wrote ${result.written} file(s)`);\n if (result.merged > 0)\n parts.push(`merged ${result.merged} file(s) cleanly`);\n if (externalizedAssets.linked > 0) {\n parts.push(\n `kept ${externalizedAssets.linked} binary asset(s) remote`,\n );\n }\n if (result.autoResolved.length > 0)\n parts.push(\n `auto-resolved ${result.autoResolved.length} conflict(s) (kept ${resolveSide})`,\n );\n if (result.deleted > 0)\n parts.push(`deleted ${result.deleted} local file(s)`);\n if (result.skipped > 0)\n parts.push(`${result.skipped} already in sync`);\n\n if (result.errors.length) {\n spinner.warn(\n `Pulled with ${result.errors.length} error(s): ${parts.join(\", \")}.`,\n );\n for (const e of result.errors) console.error(` ${e}`);\n // Every errored file is one the checkout did NOT receive, so the\n // local tree does not match the server. Conflicts already exit 1\n // below; this branch used to fall through at 0.\n //\n // Mist spawns `fluid theme pull -t <id> --root <path> --yes` to\n // clone a theme, and only records a lifecycle failure when the\n // exit code is non-zero — so a partial clone never reached the\n // agent's context and it went on to edit a checkout that was\n // missing files.\n //\n // exitCode rather than an immediate exit: the per-file error\n // list is printed right here and `.fluid-theme.json` is written\n // below, both of which still need to happen.\n process.exitCode = 1;\n } else if (result.conflicts.length > 0) {\n spinner.warn(\n `${result.conflicts.length} conflict(s) — resolve markers before pushing: ${parts.join(\", \")}.`,\n );\n console.log();\n for (const c of result.conflicts) {\n console.log(` ${chalk.yellow(\"CONFLICT\")} ${c}`);\n }\n console.log();\n console.log(\n ` Edit each file above to reconcile the ${chalk.cyan(\"<<<<<<<\")} / ${chalk.cyan(\">>>>>>>\")} markers,`,\n );\n console.log(\n ` then run ${chalk.cyan(\"fluid theme push\")} once your resolution is in place.`,\n );\n console.log();\n } else {\n spinner.succeed(parts.join(\", \") || \"Already up to date.\");\n }\n\n // Update .fluid-theme.json — baseSha is the server-side handle\n // the push preflight uses; the merge base itself lives in the\n // shadow repo.\n const remoteSha = syncer.remoteSha();\n backgroundGuard?.assertUnchanged();\n const updateConfig = () =>\n writeThemeConfig(absoluteRoot, {\n themeId: theme.id,\n themeName: theme.name,\n company: subdomain,\n baseSha: remoteSha ?? existingConfig?.baseSha,\n assetManifestSha: new ThemeAssetManifest(\n absoluteRoot,\n ).fingerprint({\n excludePending: true,\n }),\n });\n\n if (backgroundGuard)\n backgroundGuard.mutate([\".fluid-theme.json\"], updateConfig);\n else updateConfig();\n if (result.conflicts.length > 0) process.exit(1);\n } catch (error) {\n if (!(error instanceof BackgroundPullChangedError)) throw error;\n spinner?.stop();\n console.log(error.message);\n process.exitCode = 3;\n }\n },\n );\n}\n","import chalk from \"chalk\";\nimport { Command } from \"commander\";\nimport {\n findMissingSectionReferences,\n validateSchemaText,\n VALID_SETTING_TYPES,\n type BlocksSchemaType,\n type Diagnostic,\n type TemplateInput,\n} from \"@fluid-app/theme-schema\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { findLiquidBlockTagDiagnostics } from \"../theme/liquid-delimiters.js\";\nimport { findWorkspace, resolveThemeRootFromCwd } from \"../workspace.js\";\n\ninterface FileDiagnostics {\n path: string;\n diagnostics: Diagnostic[];\n}\n\n// A theme section is defined by a liquid file under the top-level `sections/`\n// directory. Returns the section name a `{% section %}` tag would reference, or\n// null if the file is not a section definition. Handles both the flat layout\n// (`sections/hero.liquid`) and the nested one (`sections/hero/index.liquid`).\nfunction sectionNameOf(relativePath: string): string | null {\n const parts = relativePath.split(/[/\\\\]/);\n if (parts[0] === \"sections\" && parts.length >= 2) {\n return parts[1]!.replace(/\\.liquid$/, \"\");\n }\n return null;\n}\n\nexport function createLintCommand(): Command {\n return new Command(\"lint\")\n .description(\"Validate theme files locally (read-only — no upload)\")\n .option(\"--root <path>\", \"Theme root directory\", \".\")\n .option(\"--json\", \"Output results as compact JSON\")\n .action(async (opts: { root: string; json?: boolean }) => {\n // Resolve the theme root the same way push/dev do: when left at the\n // default, prefer the workspace's theme root if we're inside one.\n let rootPath = opts.root;\n if (rootPath === \".\") {\n const workspace = findWorkspace();\n if (workspace) {\n rootPath = resolveThemeRootFromCwd(workspace) ?? rootPath;\n }\n }\n\n const themeRoot = new ThemeRoot(rootPath);\n if (!themeRoot.isValid()) {\n const message = `'${rootPath}' does not look like a theme directory.`;\n if (opts.json) {\n console.log(JSON.stringify({ ok: false, error: message }));\n } else {\n console.error(message);\n }\n process.exit(1);\n }\n\n const files = themeRoot.files();\n // Read each liquid file once and reuse the content for both passes\n // (validateSchemaText and the section scan) to avoid a double disk read.\n const liquidFiles = files\n .filter((f) => f.isLiquid)\n .map((f) => ({ file: f, content: f.read() }));\n\n const byFile = new Map<string, Diagnostic[]>();\n const record = (path: string, diagnostic: Diagnostic): void => {\n const existing = byFile.get(path);\n if (existing) existing.push(diagnostic);\n else byFile.set(path, [diagnostic]);\n };\n\n // ── Schema pass — the same {% schema %} validation `fluid theme push`\n // runs. blocksSchemaType mirrors ThemeFile.validateSchema: page/layout\n // templates use object blocks, sections use array blocks.\n for (const { file, content } of liquidFiles) {\n const blocksSchemaType: BlocksSchemaType = file.isTemplate\n ? \"object\"\n : \"array\";\n for (const diagnostic of validateSchemaText(content, {\n blocksSchemaType,\n })) {\n record(file.relativePath, diagnostic);\n }\n for (const diagnostic of findLiquidBlockTagDiagnostics(content)) {\n record(file.relativePath, diagnostic);\n }\n }\n\n // ── Section pass — flag `{% section 'x' %}` references to a section\n // that has no definition on disk. Section definitions and assets are\n // not themselves referrers, so they are excluded from the scan.\n const existingSectionNames = new Set<string>();\n for (const { file } of liquidFiles) {\n const name = sectionNameOf(file.relativePath);\n if (name) existingSectionNames.add(name);\n }\n const referrers: TemplateInput[] = liquidFiles\n .filter(({ file }) => sectionNameOf(file.relativePath) === null)\n .map(({ file, content }) => ({ path: file.relativePath, content }));\n for (const missing of findMissingSectionReferences(\n referrers,\n existingSectionNames,\n )) {\n record(missing.templatePath, missing.diagnostic);\n }\n\n const results: FileDiagnostics[] = [...byFile.entries()]\n .map(([path, diagnostics]) => ({ path, diagnostics }))\n .sort((a, b) => a.path.localeCompare(b.path));\n\n let errors = 0;\n let warnings = 0;\n for (const { diagnostics } of results) {\n for (const d of diagnostics) {\n if (d.severity === \"error\") errors++;\n else warnings++;\n }\n }\n\n // Surface the canonical setting types once (not in every diagnostic) so a\n // consumer fixing an \"Invalid settings type\" error has the valid set to\n // hand without it bloating each message.\n const hasInvalidSettingType = results.some(({ diagnostics }) =>\n diagnostics.some(\n (d) =>\n d.target?.kind === \"setting\" &&\n d.target.field === \"type\" &&\n d.target.settingType !== undefined,\n ),\n );\n\n if (opts.json) {\n console.log(\n JSON.stringify({\n ok: errors === 0,\n errors,\n warnings,\n filesChecked: liquidFiles.length,\n ...(hasInvalidSettingType\n ? { validSettingTypes: VALID_SETTING_TYPES }\n : {}),\n files: results,\n }),\n );\n } else {\n printText(results, errors, warnings, liquidFiles.length);\n }\n\n process.exit(errors > 0 ? 1 : 0);\n });\n}\n\nfunction plural(count: number, noun: string): string {\n return `${count} ${noun}${count === 1 ? \"\" : \"s\"}`;\n}\n\nfunction printText(\n results: FileDiagnostics[],\n errors: number,\n warnings: number,\n filesChecked: number,\n): void {\n for (const { path, diagnostics } of results) {\n console.log(chalk.bold(path));\n for (const d of diagnostics) {\n const label =\n d.severity === \"error\"\n ? chalk.red(\"error\".padEnd(7))\n : chalk.yellow(\"warning\".padEnd(7));\n // Only the first line — a few messages (e.g. the `Invalid JSON:` parse\n // error) carry a multi-line body that `--json` preserves in full.\n const message = d.message.split(\"\\n\")[0];\n console.log(` ${label} ${message}`);\n }\n }\n\n const suffix = `(${plural(filesChecked, \"file\")} checked)`;\n if (errors > 0) {\n console.log(\n `\\n${chalk.red(`✖ ${plural(errors, \"error\")}, ${plural(warnings, \"warning\")}`)} ${suffix}`,\n );\n } else if (warnings > 0) {\n console.log(\n `\\n${chalk.yellow(`⚠ ${plural(warnings, \"warning\")}`)} ${suffix}`,\n );\n } else {\n console.log(`${chalk.green(\"✓ No problems found\")} ${suffix}`);\n }\n}\n","import { Command } from \"commander\";\nimport { execFileSync } from \"node:child_process\";\nimport { rmSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport prompts from \"prompts\";\n\nconst DEFAULT_CLONE_URL = \"git@github.com:fluid-commerce/base-theme.git\";\n\nconst SAFE_NAME_RE = /^[a-zA-Z0-9_][a-zA-Z0-9._-]*$/;\n\nexport function createInitCommand(): Command {\n return new Command(\"init\")\n .description(\"Initialize a new theme by cloning the base theme\")\n .argument(\"[name]\", \"Directory name for the new theme\")\n .option(\"-u, --clone-url <url>\", \"Git URL to clone from\", DEFAULT_CLONE_URL)\n .action(async (name: string | undefined, opts: { cloneUrl: string }) => {\n if (!name) {\n const res = await prompts(\n {\n type: \"text\",\n name: \"name\",\n message: \"Theme name\",\n },\n { onCancel: () => process.exit(130) },\n );\n name = res.name as string;\n if (!name) {\n console.error(\"No name provided.\");\n process.exit(1);\n }\n }\n\n if (!SAFE_NAME_RE.test(name)) {\n console.error(\n `Invalid theme name: '${name}'. Use only letters, numbers, hyphens, underscores, and dots.`,\n );\n process.exit(1);\n }\n\n console.log(`Cloning theme from ${opts.cloneUrl} into ${name}…`);\n execFileSync(\"git\", [\"clone\", opts.cloneUrl, name], { stdio: \"inherit\" });\n\n for (const dir of [\".git\", \".github\"]) {\n const path = join(name, dir);\n if (existsSync(path)) rmSync(path, { recursive: true, force: true });\n }\n\n console.log(`\\nTheme initialized in ./${name}`);\n console.log(`Next steps:\\n cd ${name}\\n fluid theme push`);\n });\n}\n","import { Command } from \"commander\";\nimport prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { getLastDevThemeId } from \"../plugin-state.js\";\nimport { themes } from \"@fluid-app/themes-api-client\";\n\nfunction localSuggest(\n input: string,\n choices: prompts.Choice[],\n): prompts.Choice[] {\n if (!input) return choices;\n const lower = input.toLowerCase();\n return choices.filter((c) => c.title.toLowerCase().includes(lower));\n}\n\ninterface ThemeTemplate {\n id: number;\n name: string;\n themeable_type: string;\n default: boolean;\n}\n\ninterface TemplatesResponse {\n templates: ThemeTemplate[];\n}\n\nconst THEMEABLE_TYPE_MAP: Record<string, string> = {\n \"/home\": \"home_page\",\n \"/home/shop\": \"shop_page\",\n \"/home/join\": \"join_page\",\n \"/cart\": \"cart_page\",\n \"/home/blog\": \"post_page\",\n \"/home/categories\": \"category_page\",\n \"/home/collections\": \"collection_page\",\n};\n\nconst STATIC_ROUTES = [\n { label: \"Home\", path: \"/home\" },\n { label: \"Shop\", path: \"/home/shop\" },\n { label: \"Join / Sign Up\", path: \"/home/join\" },\n { label: \"Cart\", path: \"/cart\" },\n { label: \"Blog\", path: \"/home/blog\" },\n { label: \"Categories (all)\", path: \"/home/categories\" },\n { label: \"Collections (all)\", path: \"/home/collections\" },\n] as const;\n\nconst RESOURCE_ROUTES = [\n {\n label: \"Category\",\n type: \"category\",\n template: \"/home/categories/%s\",\n fallback: \"/home/categories\",\n },\n {\n label: \"Collection\",\n type: \"collection\",\n template: \"/home/collections/%s\",\n fallback: \"/home/collections\",\n },\n {\n label: \"Product\",\n type: \"product\",\n template: \"/home/products/%s\",\n fallback: \"/home/shop\",\n },\n {\n label: \"Library\",\n type: \"library\",\n template: \"/home/libraries/%s\",\n fallback: \"/home/libraries\",\n },\n {\n label: \"Post\",\n type: \"post\",\n template: \"/home/posts/%s\",\n fallback: \"/home/blog\",\n },\n {\n label: \"Media\",\n type: \"medium\",\n template: \"/home/media/%s\",\n fallback: \"/home/media\",\n },\n {\n label: \"Enrollment Pack\",\n type: \"enrollment_pack\",\n template: \"/home/enrollments/%s\",\n fallback: \"/home/join\",\n },\n {\n label: \"Page\",\n type: \"page\",\n template: \"/home/pages/%s\",\n fallback: \"/home/pages\",\n },\n] as const;\n\nasync function fetchTemplatesForType(\n api: ReturnType<typeof createApiClient>,\n themeId: number,\n themeableType: string,\n): Promise<ThemeTemplate[]> {\n const params = new URLSearchParams({\n application_theme_id: String(themeId),\n themeable_type: themeableType,\n published: \"true\",\n });\n const body = await api.get<TemplatesResponse>(\n `/api/application_theme_templates?${params}`,\n );\n return body.templates ?? [];\n}\n\nasync function selectTemplate(\n api: ReturnType<typeof createApiClient>,\n themeId: number,\n themeableType: string,\n onCancel: () => void,\n): Promise<number | null> {\n const templates = await fetchTemplatesForType(api, themeId, themeableType);\n if (templates.length <= 1) return null;\n\n const templateChoices = templates.map((t) => ({\n title: `${t.name}${t.default ? \" (default)\" : \"\"}`,\n value: t.id,\n }));\n const { templateId } = await prompts(\n {\n type: \"autocomplete\",\n name: \"templateId\",\n message: \"Select a template\",\n choices: templateChoices,\n suggest: (input: string, choices: prompts.Choice[]) =>\n Promise.resolve(localSuggest(input, choices)),\n },\n { onCancel },\n );\n\n return templateId ?? null;\n}\n\nexport function createNavigateCommand(): Command {\n return new Command(\"navigate\")\n .description(\"Interactively navigate to a route in the dev server browser\")\n .option(\"--host <host>\", \"Dev server host\", \"127.0.0.1\")\n .option(\"--port <port>\", \"Dev server port\", \"9292\")\n .option(\"-t, --theme <id>\", \"Theme ID (defaults to active dev theme)\")\n .action(async (opts: { host: string; port: string; theme?: string }) => {\n requireToken();\n\n const themeId = opts.theme ? Number(opts.theme) : getLastDevThemeId();\n\n if (!themeId) {\n console.error(\n \"No active dev theme. Run `fluid theme dev` first, or pass --theme <id>.\",\n );\n process.exit(1);\n }\n\n const address = `http://${opts.host}:${opts.port}`;\n\n type Choice = {\n title: string;\n value:\n | string\n | {\n resourceType: string;\n template: string;\n fallback: string;\n label: string;\n };\n };\n const choices: Choice[] = [\n ...STATIC_ROUTES.map((r) => ({ title: r.label, value: r.path })),\n ...RESOURCE_ROUTES.map((r) => ({\n title: `${r.label} (select specific)`,\n value: {\n resourceType: r.type,\n template: r.template,\n fallback: r.fallback,\n label: r.label,\n },\n })),\n ];\n\n const onCancel = () => process.exit(130);\n\n const { dest } = await prompts(\n {\n type: \"autocomplete\",\n name: \"dest\",\n message: \"Select a route\",\n choices,\n suggest: (input: string, choices: prompts.Choice[]) =>\n Promise.resolve(localSuggest(input, choices)),\n },\n { onCancel },\n );\n\n if (!dest) return;\n\n const api = createApiClient();\n let path: string;\n let themeableType: string | undefined;\n\n if (typeof dest === \"string\") {\n path = dest;\n themeableType = THEMEABLE_TYPE_MAP[dest];\n } else {\n themeableType = dest.resourceType;\n const body = await themes.getApplicationThemeAvailableThemeables(\n api,\n themeId,\n { themeable: dest.resourceType, per_page: 50 },\n );\n const resources = body.available_themeables ?? [];\n\n if (!resources.length) {\n console.log(`No ${dest.label} resources found, using listing page.`);\n path = dest.fallback;\n } else {\n const resourceChoices = resources.map((r) => ({\n title: r.title ?? r.slug ?? \"Untitled\",\n value: r.slug,\n }));\n const { slug } = await prompts(\n {\n type: \"autocomplete\",\n name: \"slug\",\n message: `Select a ${dest.label.toLowerCase()}`,\n choices: resourceChoices,\n suggest: (input: string, choices: prompts.Choice[]) =>\n Promise.resolve(localSuggest(input, choices)),\n },\n { onCancel },\n );\n path = dest.template.replace(\"%s\", slug as string);\n }\n }\n\n let templateParam = \"\";\n if (themeableType) {\n const templateId = await selectTemplate(\n api,\n themeId,\n themeableType,\n onCancel,\n );\n if (templateId) {\n templateParam = `?theme_template_id=${templateId}`;\n }\n }\n\n const url = `${address}${path}${templateParam}`;\n console.log(`\\nNavigating to: ${url}\\n`);\n const open = (await import(\"open\")).default;\n await open(url);\n });\n}\n","import {\n cpSync,\n existsSync,\n mkdirSync,\n readdirSync,\n renameSync,\n rmSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\n\n// A skill is a directory containing a SKILL.md. The bundled skills directory\n// holds one such directory per skill (e.g. `themes-review/`).\nexport function listSkillNames(skillsDir: string): string[] {\n if (!existsSync(skillsDir)) return [];\n return readdirSync(skillsDir, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name)\n .filter((name) => existsSync(join(skillsDir, name, \"SKILL.md\")))\n .sort();\n}\n\nexport interface InstallSkillsOptions {\n /** Directory holding the bundled skills (one sub-directory per skill). */\n sourceDir: string;\n /** Directory the skills are copied into (one sub-directory per skill). */\n targetRoot: string;\n /** Overwrite existing skills without asking. */\n force: boolean;\n /**\n * Asked once per skill that already exists when not forcing. Return true to\n * overwrite, false to leave the existing copy untouched.\n */\n confirmOverwrite: (name: string) => Promise<boolean>;\n /**\n * Called immediately for each install that left a temporary backup directory\n * behind (it could not be removed). Reported as it happens — not via the\n * return value — so the warning isn't lost if a later skill throws mid-loop.\n */\n onLeftover: (path: string) => void;\n}\n\nexport interface InstallSkillsResult {\n readonly installed: readonly string[];\n readonly skipped: readonly string[];\n}\n\n// Copy each bundled skill into `targetRoot/<name>`. Existing skills are only\n// replaced with `force` or an affirmative `confirmOverwrite`; everything else is\n// reported as skipped so the caller can summarize what happened.\nexport async function installSkills(\n options: InstallSkillsOptions,\n): Promise<InstallSkillsResult> {\n const { sourceDir, targetRoot, force, confirmOverwrite, onLeftover } =\n options;\n\n const installed: string[] = [];\n const skipped: string[] = [];\n\n mkdirSync(targetRoot, { recursive: true });\n\n for (const name of listSkillNames(sourceDir)) {\n const from = join(sourceDir, name);\n const to = join(targetRoot, name);\n const exists = existsSync(to);\n\n if (exists && !force && !(await confirmOverwrite(name))) {\n skipped.push(name);\n continue;\n }\n\n // Atomic copy + swap: a failed copy never destroys an existing install, and\n // the whole-directory replace prunes files removed from the bundled skill.\n // Report any leftover backup immediately so the warning isn't lost if a\n // later skill throws mid-loop.\n const leftover = replaceDirectory(from, to);\n if (leftover !== null) onLeftover(leftover);\n installed.push(name);\n }\n\n return { installed, skipped };\n}\n\n/**\n * Replace `target` with a fresh copy of `source` without ever leaving `target`\n * missing or partially written.\n *\n * Filesystem copies are not atomic, so a naive \"delete then copy\" loses the\n * original if the copy fails (permissions, no disk space, an interrupted\n * process). This stages the copy in a sibling directory and only swaps it into\n * place once it has fully succeeded; an existing `target` is moved aside to a\n * sibling backup first and restored if the swap fails. Because the whole\n * directory is replaced, files removed or renamed in `source` do not linger.\n *\n * Staging and backup directories live beside `target`, so its parent must\n * already exist and be on the same filesystem — that keeps the swap a cheap,\n * atomic rename rather than a cross-device copy.\n *\n * Not safe against a second process racing on the same `target`; intended for\n * single-process CLI use.\n *\n * @returns the path of a leftover backup directory that could not be removed\n * after an otherwise-successful replace (the previous contents are retained\n * there for manual cleanup), or `null` when nothing was left behind. The caller\n * should surface a non-null result so the leftover isn't silently hidden.\n */\nexport function replaceDirectory(\n source: string,\n target: string,\n): string | null {\n const staging = reserveSiblingPath(target, \"staging\");\n try {\n cpSync(source, staging, { recursive: true });\n } catch (error) {\n removeQuietly(staging);\n throw error;\n }\n\n // No existing target: a single rename moves the staged copy into place.\n if (!existsSync(target)) {\n return swapIntoPlace(staging, target, null);\n }\n\n // Existing target: move it aside first so the swap stays reversible.\n const backup = reserveSiblingPath(target, \"backup\");\n try {\n renameSync(target, backup);\n } catch (error) {\n removeQuietly(staging);\n throw error;\n }\n return swapIntoPlace(staging, target, backup);\n}\n\n// Move the staged copy into `target`. If the move fails, restore the original\n// from `backup` (when there was one) *before* any cleanup, so a failed staging\n// removal can never leave the caller without a directory at `target`; then\n// best-effort discard the staged copy and rethrow. On success, returns the\n// backup path if it could not be removed (a leftover the caller should report),\n// or `null`.\nfunction swapIntoPlace(\n staging: string,\n target: string,\n backup: string | null,\n): string | null {\n try {\n renameSync(staging, target);\n } catch (error) {\n if (backup !== null) restoreBackup(backup, target, error);\n removeQuietly(staging);\n throw error;\n }\n return removeQuietly(backup);\n}\n\n// Best-effort restore of the original directory. If even this fails, surface an\n// error pointing at the backup so the user can recover their data by hand.\nfunction restoreBackup(backup: string, target: string, cause: unknown): void {\n try {\n renameSync(backup, target);\n } catch {\n throw new Error(\n `Failed to replace ${target}; its previous contents are preserved at ${backup}.`,\n { cause },\n );\n }\n}\n\n// Best-effort removal of a temporary staging/backup directory. Cleanup must\n// never throw: a failure here should not mask the real outcome or strand the\n// caller. Returns the path when removal failed (so the caller can surface the\n// leftover), or `null` when the directory was removed or there was none.\nfunction removeQuietly(path: string | null): string | null {\n if (path === null) return null;\n try {\n rmSync(path, { recursive: true, force: true });\n return null;\n } catch {\n return path;\n }\n}\n\n// Pick a sibling path of `basePath` that does not exist yet, so renaming onto it\n// is a clean create on every platform (Windows rejects a rename onto an existing\n// directory). Deterministic — no randomness.\nfunction reserveSiblingPath(basePath: string, label: string): string {\n let candidate = `${basePath}.${label}`;\n for (let n = 1; existsSync(candidate); n += 1) {\n candidate = `${basePath}.${label}.${n}`;\n }\n return candidate;\n}\n","import { dirname, join, relative, resolve, sep } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport chalk from \"chalk\";\nimport { Command } from \"commander\";\nimport prompts from \"prompts\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { installSkills, listSkillNames } from \"../skills/install.js\";\n\n// Where skills land by default — `.agents/skills/` is the tool-neutral\n// convention for agent skills.\nconst DEFAULT_TARGET_DIR = \".agents/skills\";\n\n// The bundled skills ship beside the compiled CLI at `dist/skills/` and live at\n// the package root `skills/` in source. Resolve relative to this module so the\n// lookup works whether the code runs from the published bundle or from source.\nfunction resolveBundledSkillsDir(): string {\n const here = dirname(fileURLToPath(import.meta.url));\n const candidates = [\n join(here, \"skills\"), // dist/skills (bundled build output)\n join(here, \"..\", \"skills\"), // package root from dist/index.mjs\n join(here, \"..\", \"..\", \"skills\"), // package root from src/commands/\n ];\n for (const dir of candidates) {\n if (listSkillNames(dir).length > 0) return dir;\n }\n // Last resort: walk up looking for a skills/ dir that holds a skill.\n let dir = here;\n for (let depth = 0; depth < 6; depth++) {\n const candidate = join(dir, \"skills\");\n if (listSkillNames(candidate).length > 0) return candidate;\n dir = dirname(dir);\n }\n throw new Error(\n \"Could not locate the bundled theme skills — this is a packaging bug.\",\n );\n}\n\n// Skills stay out of `fluid theme push/pull/dev/lint` only when some path\n// segment is a dot-directory — that is the sole thing ThemeRoot.glob() skips.\n// Returns true when installing into a *visible* dir inside a theme, where the\n// files would be uploaded as theme content instead of kept local.\nfunction skillsWouldShipWithTheme(cwd: string, targetRoot: string): boolean {\n if (!new ThemeRoot(cwd).isValid()) return false; // not a theme dir — irrelevant\n const rel = relative(cwd, targetRoot);\n if (rel.startsWith(\"..\")) return false; // outside the theme root — never scanned\n return !rel.split(sep).some((segment) => segment.startsWith(\".\"));\n}\n\nexport function createSkillsCommand(): Command {\n const skills = new Command(\"skills\").description(\n \"Manage the bundled Fluid theme AI skills\",\n );\n\n skills\n .command(\"install\")\n .description(\n \"Copy the bundled theme skills into the current directory (default: .agents/skills/)\",\n )\n .option(\"-d, --dir <path>\", \"Directory to install into\", DEFAULT_TARGET_DIR)\n .option(\"-f, --force\", \"Overwrite existing skills without prompting\")\n .action(async (opts: { dir: string; force?: boolean }) => {\n const sourceDir = resolveBundledSkillsDir();\n if (listSkillNames(sourceDir).length === 0) {\n console.error(\"No bundled skills found to install.\");\n process.exit(1);\n }\n\n const targetRoot = resolve(process.cwd(), opts.dir);\n if (skillsWouldShipWithTheme(process.cwd(), targetRoot)) {\n console.log(\n `${chalk.yellow(\"⚠\")} ${chalk.bold(opts.dir)} is not a hidden directory — ` +\n `'fluid theme push' will upload these skills as theme files. ` +\n `Install into a dot-directory like ${chalk.cyan(DEFAULT_TARGET_DIR)} to keep them local.`,\n );\n }\n\n const { installed, skipped } = await installSkills({\n sourceDir,\n targetRoot,\n force: Boolean(opts.force),\n confirmOverwrite: async (name) => {\n const res = await prompts(\n {\n type: \"confirm\",\n name: \"overwrite\",\n message: `${chalk.yellow(name)} already exists in ${opts.dir}. Overwrite?`,\n initial: false,\n },\n { onCancel: () => process.exit(130) },\n );\n return Boolean(res.overwrite);\n },\n // Warn as soon as a leftover is found, so it's reported even if a later\n // skill fails before the install finishes.\n onLeftover: (path) => {\n console.log(\n `${chalk.yellow(\"⚠\")} kept the previous copy at ${path} (couldn't remove it — delete it manually)`,\n );\n },\n });\n\n for (const name of installed) {\n console.log(`${chalk.green(\"✓\")} ${name} → ${join(opts.dir, name)}`);\n }\n for (const name of skipped) {\n console.log(`${chalk.dim(`· skipped ${name} (kept existing)`)}`);\n }\n\n const parts = [\n installed.length > 0 ? `${installed.length} installed` : null,\n skipped.length > 0 ? `${skipped.length} skipped` : null,\n ].filter(Boolean);\n console.log(\n `\\n${chalk.bold(parts.join(\", \") || \"Nothing to do\")} in ${targetRoot}`,\n );\n if (installed.length > 0) {\n console.log(\n chalk.dim(\"Restart your agent session to pick up the new skills.\"),\n );\n }\n });\n\n return skills;\n}\n","import { Command } from \"commander\";\nimport type { PluginContext } from \"@fluid-app/fluid-cli\";\nimport { createDevCommand } from \"./dev.js\";\nimport { createPushCommand } from \"./push.js\";\nimport { createPullCommand } from \"./pull.js\";\nimport { createLintCommand } from \"./lint.js\";\nimport { createInitCommand } from \"./init.js\";\nimport { createNavigateCommand } from \"./navigate.js\";\nimport { createSkillsCommand } from \"./skills.js\";\n\nexport function registerThemeCommand(ctx: PluginContext): void {\n const cmd = new Command(\"theme\").description(\n \"Theme developer workflow — dev server, push, pull, lint, init, skills\",\n );\n\n cmd.addCommand(createDevCommand());\n cmd.addCommand(createPushCommand());\n cmd.addCommand(createPullCommand());\n cmd.addCommand(createLintCommand());\n cmd.addCommand(createInitCommand());\n cmd.addCommand(createNavigateCommand());\n cmd.addCommand(createSkillsCommand());\n\n ctx.program.addCommand(cmd);\n}\n","import type { FluidPlugin, PluginContext } from \"@fluid-app/fluid-cli\";\nimport { registerThemeCommand } from \"./commands/theme.js\";\n\nconst plugin: FluidPlugin = {\n name: \"@fluid-app/fluid-cli-theme-dev\",\n version: \"0.1.0\",\n register(ctx: PluginContext) {\n registerThemeCommand(ctx);\n },\n};\n\nexport default plugin;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAgDA,SAAgB,eAAe,OAA8B;AAC3D,KAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CAC7D,QAAO;AAGT,QAAO;;;;;;;;AAST,SAAgB,eAAe,MAAkC;AAC/D,KAAI,CAAC,KACH,QAAO;AAGT,QAAQ,KAAK,UAAU;;;;;;;ACOzB,IAAa,WAAb,MAAa,iBAAiB,MAAM;CAClC;;;;;CAMA;;;;;CAMA;CAEA;CAEA,YACE,SACA,QACA,MACA,WACA,MACA;AACA,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,SAAS;AACd,OAAK,OAAO,QAAQ;AACpB,OAAK,OAAO,QAAQ;AACpB,OAAK,YAAY;AAEjB,MAAI,uBAAuB,MAEvB,OAMA,kBAAkB,MAAM,SAAS;;CAIvC,SAOE;AACA,SAAO;GACL,MAAM,KAAK;GACX,SAAS,KAAK;GACd,QAAQ,KAAK;GACb,MAAM,KAAK;GACX,MAAM,KAAK;GACX,WAAW,KAAK;GACjB;;;AAIL,SAAS,mBAAmB,OAAoC;AAC9D,KAAI,OAAO,UAAU,SACnB;CAGF,MAAM,UAAU,MAAM,MAAM;AAC5B,QAAO,QAAQ,SAAS,IAAI,UAAU,KAAA;;AAGxC,SAAS,wBAAwB,SAAsC;AACrE,QACE,mBAAmB,QAAQ,IAAI,eAAe,CAAC,IAC/C,mBAAmB,QAAQ,IAAI,aAAa,CAAC,IAC7C,mBAAmB,QAAQ,IAAI,eAAe,CAAC;;AAInD,SAAS,yBAAyB,MAAmC;AACnE,KAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,KAAK,CAC1D;CAGF,MAAM,SAAS;CACf,MAAM,OAAO,OAAO;AAEpB,QACE,mBAAmB,OAAO,WAAW,IACrC,mBAAmB,OAAO,UAAU,KACnC,QAAQ,OAAO,SAAS,YAAY,CAAC,MAAM,QAAQ,KAAK,GACpD,mBAAoB,KAAiC,WAAW,IACjE,mBAAoB,KAAiC,UAAU,GAC/D,KAAA;;;;;AAOR,SAAgB,WAAW,OAAmC;AAC5D,QAAO,iBAAiB;;;;;AA4C1B,SAAgB,kBACd,QACqB;CACrB,MAAM,EACJ,SACA,cACA,aACA,iBAAiB,EAAE,EACnB,aACA,OACA,cACA,qBAAqB,UACnB;CACJ,MAAM,oBAAoB,KAAK,IAAI,GAAG,cAAc,cAAc,EAAE;CACpE,MAAM,0BAA0B,KAAK,IAAI,GAAG,cAAc,eAAe,EAAE;;;;CAK3E,eAAe,aACb,eACiC;EACjC,MAAM,UAAkC;GACtC,QAAQ;GACR,gBAAgB;GAChB,GAAG;GACH,GAAG;GACJ;AAGD,MAAI,cAAc;GAChB,MAAM,QAAQ,MAAM,cAAc;AAClC,OAAI,MACF,SAAQ,gBAAgB,UAAU;;AAItC,SAAO;;;;;;;CAQT,SAAS,QAAQ,UAA0B;AACzC,SAAO,GAAG,UAAU;;;;;;CAOtB,SAAS,SACP,UACA,QACQ;EACR,MAAM,UAAU,QAAQ,SAAS;AAEjC,MAAI,CAAC,UAAU,OAAO,KAAK,OAAO,CAAC,WAAW,EAC5C,QAAO;EAGT,MAAM,cAAc,IAAI,iBAAiB;AAEzC,SAAO,QAAQ,OAAO,CAAC,SAAS,CAAC,KAAK,WAAW;AAC/C,OAAI,UAAU,KAAA,KAAa,UAAU,KACnC;AAGF,OAAI,MAAM,QAAQ,MAAM,CAEtB,OAAM,SAAS,SAAS,YAAY,OAAO,GAAG,IAAI,KAAK,OAAO,KAAK,CAAC,CAAC;YAC5D,OAAO,UAAU,SAE1B,QAAO,QAAQ,MAAM,CAAC,SAAS,CAAC,QAAQ,cAAc;AACpD,QAAI,aAAa,KAAA,KAAa,aAAa,KACzC;AAGF,QAAI,MAAM,QAAQ,SAAS,CACzB,UAAS,SAAS,SAChB,YAAY,OAAO,GAAG,IAAI,GAAG,OAAO,MAAM,OAAO,KAAK,CAAC,CACxD;QAED,aAAY,OAAO,GAAG,IAAI,GAAG,OAAO,IAAI,OAAO,SAAS,CAAC;KAE3D;OAEF,aAAY,OAAO,KAAK,OAAO,MAAM,CAAC;IAExC;EAEF,MAAM,KAAK,YAAY,UAAU;AACjC,SAAO,KAAK,GAAG,QAAQ,GAAG,OAAO;;;;;;CAOnC,eAAe,eACb,UACA,QACA,MACoB;EACpB,MAAM,kBAAkB,wBAAwB,SAAS,QAAQ;AAEjE,MAAI,SAAS,WAAW,OAAO,YAC7B,cAAa;AAGf,MAAI,CAAC,SAAS,IAAI;GAGhB,MAAM,YAAY,MAAM,SAAS,MAAM,CAAC,YAAY,GAAG;AAGvD,OAFoB,SAAS,QAAQ,IAAI,eAAe,EAEvC,SAAS,mBAAmB,EAAE;IAI7C,IAAI;AACJ,QAAI;AACF,cAAS,KAAK,MAAM,UAAU;YACxB;AACN,WAAM,IAAI,SACR,UAAU,MAAM,GAAG,IAAI,IACrB,GAAG,OAAO,8BAA8B,SAAS,UACnD,SAAS,QACT,MACA,gBACD;;IAGH,MAAM,OAAO,eAAe,OAAO;AA2BnC,UAAM,IAAI,UAvBE,cACD;KACL,MAAM,cACJ,OAAO,KAAK,UAAU,YAAY,KAAK,UAAU,OAC5C,KAAK,MAAgC,UACtC,KAAA;KACN,MAAM,cACJ,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ,KAAA;KAChD,MAAM,UACJ,OAAO,KAAK,YAAY,WAAW,KAAK,UAAU,KAAA;KACpD,MAAM,eACJ,OAAO,KAAK,kBAAkB,WAC1B,KAAK,gBACL,KAAA;AACN,YACE,WACA,gBACA,gBACC,OAAO,gBAAgB,WAAW,cAAc,KAAA;QAEjD,GACJ,KAAA,MAGK,GAAG,OAAO,8BAA8B,SAAS,UACxD,SAAS,QACT,OAAO,eAAe,KAAK,GAAI,QAC/B,mBAAmB,yBAAyB,OAAO,EACnD,KACD;SAED,OAAM,IAAI,SACR,GAAG,OAAO,8BAA8B,SAAS,UACjD,SAAS,QACT,MACA,gBACD;;AAIL,MACE,SAAS,WAAW,OACpB,SAAS,QAAQ,IAAI,iBAAiB,KAAK,IAE3C,QAAO;AAKT,MAFoB,SAAS,QAAQ,IAAI,eAAe,EAEvC,SAAS,mBAAmB,EAAE;GAC7C,MAAM,eAAe,MAAM,SAAS,MAAM;AAE1C,OAAI;AAEF,WADa,KAAK,MAAM,aAAa;WAE/B;AACN,QAAI,mBACF,OAAM,IAAI,SACR,oCACA,SAAS,QACT,MACA,gBACD;AAKH,WAAO,eAAgB,eAA8B;;;AAKzD,SAAO;;CAGT,SAAS,uBAAuB,cAA8B;AAC5D,SAAO,0BAA0B,MAAM,eAAe;;CAGxD,eAAe,oBAAoB,cAAqC;EACtE,MAAM,UAAU,uBAAuB,aAAa;AACpD,MAAI,WAAW,EACb;AAGF,QAAM,IAAI,SAAS,YAAY,WAAW,SAAS,QAAQ,CAAC;;CAG9D,eAAe,sBACb,KACA,cACA,QACmB;EACnB,IAAI,aAAa;AAEjB,SAAO,KACL,KAAI;AACF,UAAO,MAAM,MAAM,KAAK,aAAa;WAC9B,cAAc;AACrB,OAAI,QAAQ,WAAW,cAAc,kBACnC,OAAM;AAGR,iBAAc;AACd,SAAM,oBAAoB,WAAW;AAErC,OAAI,QAAQ,QACV,OAAM;;;;;;CASd,eAAe,QACb,UACA,UAA0B,EAAE,EACR;EACpB,MAAM,EACJ,SAAS,OACT,SAAS,eACT,QACA,MACA,QACA,aACE;EAEJ,MAAM,MAAM,SAAS,SAAS,UAAU,OAAO,GAAG,QAAQ,SAAS;EAEnE,MAAM,UAAU,MAAM,aAAa,cAAc;EAEjD,IAAI;AAEJ,MAAI;GACF,MAAM,eAA4B;IAAE;IAAQ;IAAS;AACrD,OAAI,YAAa,cAAa,cAAc;AAC5C,OAAI,MAAO,cAAa,QAAQ;AAChC,OAAI,SAAU,cAAa,WAAW;GACtC,MAAM,iBACJ,QAAQ,WAAW,QAAQ,KAAK,UAAU,KAAK,GAAG;AACpD,OAAI,eAAgB,cAAa,OAAO;AACxC,OAAI,OAAQ,cAAa,SAAS;AAClC,cAAW,MAAM,sBAAsB,KAAK,cAAc,OAAO;WAC1D,cAAc;AACrB,SAAM,IAAI,SACR,kBAAkB,wBAAwB,QAAQ,aAAa,UAAU,2BACzE,GACA,KACD;;AAGH,SAAO,eAA0B,UAAU,QAAQ,IAAI;;;;;CAMzD,eAAe,oBACb,UACA,UACA,UAEI,EAAE,EACc;EACpB,MAAM,EACJ,SAAS,QACT,SAAS,eACT,QACA,aACE;EAEJ,MAAM,MAAM,QAAQ,SAAS;EAC7B,MAAM,UAAU,MAAM,aAAa,cAAc;AAGjD,SAAO,QAAQ;EAEf,IAAI;AAEJ,MAAI;GACF,MAAM,eAA4B;IAAE;IAAQ;IAAS,MAAM;IAAU;AACrE,OAAI,YAAa,cAAa,cAAc;AAC5C,OAAI,MAAO,cAAa,QAAQ;AAChC,OAAI,SAAU,cAAa,WAAW;AACtC,OAAI,OAAQ,cAAa,SAAS;AAClC,cAAW,MAAM,sBAAsB,KAAK,cAAc,OAAO;WAC1D,cAAc;AACrB,SAAM,IAAI,SACR,kBAAkB,wBAAwB,QAAQ,aAAa,UAAU,2BACzE,GACA,KACD;;AAGH,SAAO,eAA0B,UAAU,QAAQ,IAAI;;AAIzD,QAAO;EACI;EACY;EAGrB,MACE,UACA,QACA,YAEA,QAAmB,UAAU;GAC3B,GAAG;GACH,QAAQ;GACR,GAAI,UAAU,EAAE,QAAQ;GACzB,CAAC;EAEJ,OACE,UACA,MACA,YAEA,QAAmB,UAAU;GAC3B,GAAG;GACH,QAAQ;GACR;GACD,CAAC;EAEJ,MACE,UACA,MACA,YAEA,QAAmB,UAAU;GAC3B,GAAG;GACH,QAAQ;GACR;GACD,CAAC;EAEJ,QACE,UACA,MACA,YAEA,QAAmB,UAAU;GAC3B,GAAG;GACH,QAAQ;GACR;GACD,CAAC;EAEJ,SACE,UACA,YAEA,QAAmB,UAAU;GAC3B,GAAG;GACH,QAAQ;GACT,CAAC;EACL;;;;;AC9lBH,SAAS,aAAqB;AAC5B,QAAO,QAAQ,IAAI,qBAAqB;;AAK1C,IAAI,iBAAiB,QAAQ,SAAS;AACtC,eAAe,gBAAgB,eAAgD;AAC7E,kBAAiB,eAAe,WACxB,IAAI,SAAS,YAAY,WAAW,SAAS,IAAM,CAAC,CAC3D;AACD,OAAM;AACN,QAAO,iBAAiB,cAAc,IAAI;;AAG5C,SAAgB,gBAAgB,eAAmC;AACjE,QAAO,kBAAkB;EACvB,SAAS,YAAY;EACrB,oBACE,QAAQ,IAAI,6BAA6B,MACrC,gBAAgB,cAAc,GAC7B,iBAAiB,cAAc,IAAI;EAC3C,CAAC;;AAGJ,SAAgB,eAAuB;CACrC,MAAM,QAAQ,cAAc;AAC5B,KAAI,CAAC,OAAO;AACV,UAAQ,MAAM,0CAA0C;AACxD,UAAQ,KAAK,EAAE;;AAEjB,QAAO;;;;ACTT,MAAM,cAAc;;;;;;;;AASpB,SAAS,iBAAiB,SAAyB;AAIjD,QAHa,QACV,QAAQ,4BAA4B,GAAG,CACvC,QAAQ,YAAY,GAAG,CACd,QAAQ,kBAAkB,GAAG;;AAG3C,SAAS,WAAW,WAA2B;AAC7C,QAAO,KAAK,WAAW,YAAY;;;AAIrC,SAAgB,gBAAgB,WAAuC;CACrE,MAAM,OAAO,WAAW,UAAU;AAClC,KAAI,CAAC,WAAW,KAAK,CAAE,QAAO;AAC9B,KAAI;EACF,MAAM,MAAM,aAAa,MAAM,QAAQ;EACvC,MAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,MAAI,OAAO,OAAO,YAAY,SAC5B,QAAO,UAAU,iBAAiB,OAAO,QAAQ;AAEnD,SAAO;SACD;AACN,SAAO;;;;;;;;AASX,SAAgB,sBACd,WAC0B;CAC1B,MAAM,OAAO,WAAW,UAAU;AAClC,KAAI,CAAC,WAAW,KAAK,CAAE,QAAO;AAC9B,KAAI;EACF,MAAM,MAAM,aAAa,MAAM,QAAQ;AACvC,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,SAAO;;;;AAKX,SAAgB,iBAAiB,WAAmB,QAA2B;AAE7E,eADa,WAAW,UAAU,EACd,KAAK,UAAU,QAAQ,MAAM,EAAE,GAAG,MAAM,QAAQ;;;;AC1DtE,MAAM,aAAa;AAEnB,SAAS,WAA0B;AAEjC,QADe,YAAY,CACZ,QAAQ,eAAiC,EAAE;;;AAI5D,SAAS,iBAAiB,KAAqB;CAC7C,MAAM,MAAM,IAAI,QAAQ,IAAI;AAC5B,QAAO,QAAQ,KAAK,MAAM,IAAI,MAAM,MAAM,EAAE;;;;;;;AAQ9C,SAAS,aACP,UACA,KACA,OAC6B;CAC7B,MAAM,OAAoC,EAAE;AAC5C,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,YAAY,EAAE,CAAC,CACjD,KAAI,WAAW,iBAAiB,EAAE,CAAC,CAAE,MAAK,KAAK;AAEjD,MAAK,OAAO;AACZ,QAAO;;;;;;;AAQT,SAAgB,YACd,SACA,WACQ;AACR,QAAO,GAAG,WAAW,UAAU,GAAG;;;;;;;AAQpC,SAAgB,YAAY,KAAsC;CAChE,MAAM,QAAQ,UAAU;CACxB,MAAM,WAAW,MAAM,YAAY;AACnC,KAAI,SAAU,QAAO;AAErB,KAAI,MAAM,YAAY;EACpB,MAAM,WAAwB;GAC5B,IAAI,MAAM;GACV,MAAM,MAAM,gBAAgB,gBAAgB,MAAM;GACnD;AACD,gBAAc,WAAW;GAEvB,MAAM,EAAE,YAAY,KAAK,cAAc,OAAO,GAAG,SADhC,OAAO,QAAQ,eAAiC,EAAE;AAEnE,UAAO;IACL,GAAG;IACH,SAAS;KACP,GAAG,OAAO;MACT,aAAa;MACZ,GAAG;MACH,WAAW,aAAa,KAAK,WAAW,KAAK,SAAS;MACtD,gBAAgB,SAAS;MAC1B;KACF;IACF;IACD;AACF,SAAO;;;;AAOX,SAAgB,YAAY,KAAa,OAA0B;AACjE,eAAc,WAAW;EACvB,MAAM,UAAW,OAAO,QAAQ,eAAiC,EAAE;AACnE,SAAO;GACL,GAAG;GACH,SAAS;IACP,GAAG,OAAO;KACT,aAAa;KACZ,GAAG;KACH,WAAW,aAAa,QAAQ,WAAW,KAAK,MAAM;KACtD,gBAAgB,MAAM;KACvB;IACF;GACF;GACD;;;AAIJ,SAAgB,cAAc,KAAmB;AAC/C,eAAc,WAAW;EACvB,MAAM,UAAW,OAAO,QAAQ,eAAiC,EAAE;EACnE,MAAM,UAAU,QAAQ,YAAY;AACpC,MAAI,CAAC,QAAS,QAAO;EACrB,MAAM,GAAG,MAAM,UAAU,GAAG,SAAS,QAAQ,aAAa,EAAE;EAC5D,MAAM,OAAsB;GAAE,GAAG;GAAS,WAAW;GAAM;AAE3D,MAAI,QAAQ,mBAAmB,QAAQ,GACrC,MAAK,iBAAiB,KAAA;AAExB,SAAO;GACL,GAAG;GACH,SAAS;IAAE,GAAG,OAAO;KAAU,aAAa;IAAM;GACnD;GACD;;;;;;;AAQJ,SAAgB,kBAAkB,IAAkB;AAClD,eAAc,WAAW;EACvB,MAAM,UAAW,OAAO,QAAQ,eAAiC,EAAE;AACnE,SAAO;GACL,GAAG;GACH,SAAS;IACP,GAAG,OAAO;KACT,aAAa;KAAE,GAAG;KAAS,gBAAgB;KAAI;IACjD;GACF;GACD;;;;;;;AAQJ,SAAgB,oBAAwC;CACtD,MAAM,QAAQ,UAAU;AACxB,QAAO,MAAM,kBAAkB,MAAM;;;;AC3KvC,MAAM,aAAqC;CACzC,WAAW;CACX,SAAS;CACT,QAAQ;CACR,OAAO;CACP,SAAS;CACT,QAAQ;CACR,OAAO;CACP,QAAQ;CACT;AAED,MAAM,eAAuC;CAC3C,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,SAAS;CACT,UAAU;CACV,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,QAAQ;CACT;AAOD,SAAgB,YAAY,KAAuB;CACjD,MAAM,OAAO,WAAW;AACxB,KAAI,KAAM,QAAO;EAAE,MAAM;EAAM,QAAQ;EAAM;CAE7C,MAAM,SAAS,aAAa;AAC5B,KAAI,OAAQ,QAAO;EAAE,MAAM;EAAQ,QAAQ;EAAO;AAElD,QAAO;EAAE,MAAM;EAA4B,QAAQ;EAAO;;;;AC3C5D,MAAM,4BAA4B,IAAI,IAAI;CACxC;CACA;CACA;CACD,CAAC;AAEF,MAAM,gCAAgC,IAAI,IAAI;CAC5C;CACA;CACA;CACD,CAAC;AAEF,SAAS,gBAAgB,KAAsB;AAE7C,QADiB,IAAI,MAAM,IAAI,CACf,OACb,YAAY,QAAQ,SAAS,KAAK,YAAY,OAAO,YAAY,KACnE;;AAGH,SAAgB,0BAA0B,OAAuB;AAC/D,QAAO,MAAM,WAAW,MAAM,IAAI;;;;;;;AAQpC,SAAgB,mBAAmB,cAA+B;CAChE,MAAM,MAAM,0BAA0B,aAAa;AACnD,KAAI,CAAC,gBAAgB,IAAI,CAAE,QAAO;AAClC,KAAI,0BAA0B,IAAI,IAAI,CAAE,QAAO;CAE/C,MAAM,WAAW,IAAI,MAAM,IAAI;CAC/B,MAAM,SAAS,SAAS;CACxB,MAAM,WAAW,SAAS,GAAG,GAAG;AAChC,KAAI,CAAC,UAAU,CAAC,YAAY,SAAS,SAAS,EAAG,QAAO;AAExD,KACE,WAAW,YACX,WAAW,YACX,WAAW,aACX,WAAW,UAEX,QAAO;AAGT,QAAO,SAAS,UAAU,KAAK,8BAA8B,IAAI,SAAS;;;;AEe5E,MAAa,sBAAyC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAE5D,CAAC,MAAM;;;;;;;;;;ACtDR,SAAgB,0BAA0B,MAAsB;AAC9D,QAAO,2BAA2B,KAAK;;AAGzC,SAAgB,iBAAiB,UAAmC;CAClE,MAAM,cAA4B,EAAE;CACpC,MAAM,sBAAM,IAAI,KAAa;AAE7B,MAAK,IAAI,QAAQ,GAAG,QAAQ,SAAS,QAAQ,SAAS;EACpD,MAAM,MAAM,SAAS;EACrB,MAAM,UACJ,QAAQ,QAAQ,OAAO,QAAQ,YAAY,CAAC,MAAM,QAAQ,IAAI,GACzD,MACD,EAAE;EAER,MAAM,KAAK,OAAO,QAAQ,OAAO,WAAW,QAAQ,KAAK,KAAA;EACzD,MAAM,OAAO,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO,KAAA;AAE/D,MAAI,OAAO,KAAA,KAAa,GAAG,MAAM,KAAK,GACpC,aAAY,KAAK;GACf,UAAU;GACV,SAAS;GACT,QAAQ;IAAE,MAAM;IAAW;IAAO,aAAa;IAAM,OAAO;IAAM;GACnE,CAAC;WACO,MAAM,IAAI,IAAI,GAAG,CAC1B,aAAY,KAAK;GACf,UAAU;GACV,SAAS,oCAAoC,GAAG;GAChD,QAAQ;IAAE,MAAM;IAAW;IAAO,WAAW;IAAI,OAAO;IAAM;GAC/D,CAAC;WACO,GACT,KAAI,IAAI,GAAG;AAGb,MAAI,CAAC,KACH,aAAY,KAAK;GACf,UAAU;GACV,SAAS,qBAAqB,MAAM,MAAM;GAC1C,QAAQ;IAAE,MAAM;IAAW;IAAO,WAAW;IAAI,OAAO;IAAQ;GACjE,CAAC;WACO,CAAC,oBAAoB,SAAS,KAAK,CAC5C,aAAY,KAAK;GACf,UAAU;GACV,SAAS,0BAA0B,KAAK;GACxC,QAAQ;IAAE,MAAM;IAAW;IAAO,aAAa;IAAM,OAAO;IAAQ;GACrE,CAAC;;AAIN,QAAO;;;;ACxDT,SAAgB,eAAe,QAAiC;CAC9D,MAAM,cAA4B,EAAE;CACpC,MAAM,wBAAQ,IAAI,KAAa;AAE/B,MAAK,IAAI,QAAQ,GAAG,QAAQ,OAAO,QAAQ,SAAS;EAClD,MAAM,MAAM,OAAO;EACnB,MAAM,QACJ,QAAQ,QAAQ,OAAO,QAAQ,YAAY,CAAC,MAAM,QAAQ,IAAI,GACzD,MACD,EAAE;EAER,MAAM,OAAO,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO,KAAA;EAC3D,MAAM,OAAO,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO,KAAA;EAC3D,MAAM,WAAW,MAAM;AAEvB,MAAI,CAAC,KACH,aAAY,KAAK;GACf,UAAU;GACV,SAAS,4BAA4B,MAAM;GAC3C,QAAQ;IAAE,MAAM;IAAS;IAAO,OAAO;IAAQ;GAChD,CAAC;WACO,MAAM,IAAI,KAAK,CACxB,aAAY,KAAK;GACf,UAAU;GACV,SAAS,sCAAsC,KAAK;GACpD,QAAQ;IAAE,MAAM;IAAS;IAAO,WAAW;IAAM,OAAO;IAAQ;GACjE,CAAC;MAEF,OAAM,IAAI,KAAK;AAMjB,MAAI,CAAC,QAAQ,SAAS,UAAU,SAAS,YAAY,EAD7B,CAAC,QAAQ,CAAC,UAEhC,aAAY,KAAK;GACf,UAAU;GACV,SAAS,mBAAmB,QAAQ,MAAM;GAC1C,QAAQ;IAAE,MAAM;IAAS;IAAO,WAAW;IAAM,OAAO;IAAQ;GACjE,CAAC;AAGJ,MAAI,SACF,KAAI,CAAC,MAAM,QAAQ,SAAS,CAE1B,aAAY,KAAK;GACf,UAAU;GACV,SAAS,mBAAmB,QAAQ,MAAM;GAC1C,QAAQ;IAAE,MAAM;IAAS;IAAO,WAAW;IAAM,OAAO;IAAY;GACrE,CAAC;MAEF,aAAY,KAAK,GAAG,iBAAiB,SAAS,CAAC;AAMnD,MAAI,MAAM,QAAQ,MAAM,OAAO,CAC7B,aAAY,KAAK,GAAG,eAAe,MAAM,OAAoB,CAAC;;AAIlE,QAAO;;;;AC5DT,SAAS,oBAAoB,MAAsB;AACjD,QAAO,KAAK,QACV,8DACA,GACD;;AAKH,SAAS,wBAAwB,UAA0B;CACzD,IAAI,QAAQ;CACZ,MAAM,QAID,EAAE;CAEP,IAAI,IAAI;AAER,QAAO,IAAI,SAAS,QAAQ;EAC1B,MAAM,KAAK,SAAS,WAAW,EAAE;AAGjC,MAAI,OAAO,MAAQ,OAAO,MAAQ,OAAO,MAAQ,OAAO,GAAM;AAC5D;AACA;;AAGF,MAAI,OAAO,KAAM;AAEf,SAAM,KAAK;IAAE,MAAM;IAAU,sBAAM,IAAI,KAAK;IAAE,cAAc;IAAM,CAAC;AAEnE;aACS,OAAO,KAAM;AAEtB,SAAM,KAAK;AAEX;aACS,OAAO,IAAM;AAGtB,SAAM,KAAK;IAAE,MAAM;IAAS,sBAAM,IAAI,KAAK;IAAE,cAAc;IAAO,CAAC;AACnE;aACS,OAAO,IAAM;AAEtB,SAAM,KAAK;AAEX;aACS,OAAO,GAEhB;WACS,OAAO,IAAM;GAEtB,MAAM,MAAM,MAAM,MAAM,SAAS;AACjC,OAAI,KAAK,SAAS,SAChB,KAAI,eAAe;AAGrB;aACS,OAAO,IAAM;GAEtB,IAAI,IAAI,IAAI;AACZ,UAAO,IAAI,SAAS,QAAQ;AAC1B,QACE,SAAS,WAAW,EAAE,KAAK,MAC3B,SAAS,WAAW,IAAI,EAAE,KAAK,GAE/B;AAEF;;GAEF,MAAM,MAAM,SAAS,MAAM,IAAI,GAAG,EAAE;AACpC,OAAI,IAAI;GAER,MAAM,MAAM,MAAM,MAAM,SAAS;AACjC,OAAI,KAAK,SAAS,YAAY,IAAI,cAAc;AAC9C,QAAI,QAAQ,YAAY,IAAI,KAAK,IAAI,IAAI,CACvC;AAEF,QAAI,KAAK,IAAI,IAAI;AACjB,QAAI,eAAe;;QAOrB;;AAIJ,QAAO;;AAST,SAAgB,mBACd,MACA,SACc;CACd,MAAM,mBAAmB,SAAS,oBAAoB;CACtD,MAAM,cAA4B,EAAE;CAGpC,MAAM,QADW,oBAAoB,KAAK,CACnB,MACrB,4DACD;AACD,KAAI,CAAC,MAAO,QAAO;CAEnB,MAAM,WAAW,MAAM,MAAM;CAE7B,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,SAAS;UACtB,GAAG;AACV,cAAY,KAAK;GACf,UAAU;GACV,SAAS,mBAAoB,EAAY;GAC1C,CAAC;AACF,SAAO;;CAIT,MAAM,QAAQ,wBAAwB,SAAS;AAC/C,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,IACzB,aAAY,KAAK;EACf,UAAU;EACV,SAAS;EACV,CAAC;AAIJ,KACE,WAAW,QACX,OAAO,WAAW,YAClB,MAAM,QAAQ,OAAO,SAAS,CAE9B,aAAY,KAAK,GAAG,iBAAiB,OAAO,SAAS,CAAC;AAIxD,KAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,YAAY,QAAQ;EACvE,MAAM,SAAS,OAAO;AAEtB,MAAI,qBAAqB,QACvB,KAAI,CAAC,MAAM,QAAQ,OAAO,CACxB,aAAY,KAAK;GACf,UAAU;GACV,SAAS;GACV,CAAC;MAEF,aAAY,KAAK,GAAG,eAAe,OAAO,CAAC;WAEpC,qBAAqB;OAE5B,MAAM,QAAQ,OAAO,IACrB,OAAO,WAAW,YAClB,WAAW,KAEX,aAAY,KAAK;IACf,UAAU;IACV,SAAS;IACV,CAAC;aAIA,MAAM,QAAQ,OAAO,CACvB,aAAY,KAAK,GAAG,eAAe,OAAO,CAAC;;AAKjD,QAAO;;;;ACjLT,MAAM,uBACJ;AAIF,MAAM,qBACJ;AAOF,MAAM,sBACJ;AAMF,MAAM,yBAAyB,IAAI,IAAI;CAAC;CAAU;CAAkB;CAAS,CAAC;AAK9E,MAAM,wBAAwB;;AAG9B,SAAgB,sBAAsB,MAAuB;AAC3D,QAAO,uBAAuB,IAAI,KAAK,IAAI,sBAAsB,KAAK,KAAK;;AAsB7E,SAAS,aAAa,QAAwB;AAC5C,QAAO,OACJ,QAAQ,sBAAsB,GAAG,CACjC,QAAQ,oBAAoB,GAAG;;;;;;;;AASpC,SAAgB,yBAAyB,QAAoC;CAC3E,MAAM,OAAO,aAAa,OAAO;CACjC,MAAM,UAAU,IAAI,OAAO,qBAAqB,IAAI;CACpD,MAAM,aAAiC,EAAE;CACzC,IAAI,QAAQ;CACZ,IAAI;AACJ,SAAQ,QAAQ,QAAQ,KAAK,KAAK,MAAM,MAAM;EAC5C,MAAM,OAAO,MAAM;AACnB,MAAI,CAAC,KAAM;AACX,aAAW,KAAK;GAAE;GAAM,IAAI,MAAM;GAAI,SAAS,MAAM;GAAI,OAAO;GAAS,CAAC;;AAE5E,QAAO;;;;;;;;;AAkCT,SAAgB,6BACd,WACA,sBACqB;CACrB,MAAM,UAA+B,EAAE;AACvC,MAAK,MAAM,YAAY,WAAW;EAChC,MAAM,2BAAW,IAAI,KAAa;AAClC,OAAK,MAAM,aAAa,yBAAyB,SAAS,QAAQ,EAAE;AAClE,OAAI,qBAAqB,IAAI,UAAU,KAAK,CAAE;AAC9C,OAAI,sBAAsB,UAAU,KAAK,CAAE;AAC3C,OAAI,SAAS,IAAI,UAAU,KAAK,CAAE;AAClC,YAAS,IAAI,UAAU,KAAK;AAC5B,WAAQ,KAAK;IACX,cAAc,SAAS;IACvB,aAAa,UAAU;IACvB,YAAY;KACV,UAAU;KACV,SAAS,+BAA+B,UAAU,KAAK;KACvD,QAAQ;MACN,MAAM;MACN,aAAa,UAAU;MACvB,OAAO,UAAU;MAClB;KACF;IACF,CAAC;;;AAGN,QAAO;;;;ACxHT,MAAM,oBAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,IAAa,YAAb,MAAuB;CACrB;CACA;CACA;CAEA,YAAY,cAAsB,MAAc;AAC9C,OAAK,eAAe;AACpB,OAAK,eAAe,0BAA0B,SAAS,MAAM,aAAa,CAAC;AAC3E,OAAK,OAAO,YAAY,QAAQ,aAAa,CAAC,aAAa,CAAC;;CAG9D,IAAI,OAAe;AACjB,SAAO,SAAS,KAAK,aAAa;;CAGpC,IAAI,SAAkB;AACpB,SAAO,KAAK,KAAK;;CAGnB,IAAI,WAAoB;AACtB,SAAO,KAAK,aAAa,SAAS,UAAU;;CAG9C,IAAI,SAAkB;AACpB,SAAO,KAAK,aAAa,SAAS,QAAQ;;CAG5C,IAAI,SAAkB;AACpB,SAAO,WAAW,KAAK,aAAa;;CAGtC,OAAe;AACb,SAAO,aAAa,KAAK,cAAc,QAAQ;;CAGjD,aAAqB;AACnB,SAAO,aAAa,KAAK,aAAa;;CAGxC,MAAM,SAAgC;AACpC,YAAU,QAAQ,KAAK,aAAa,EAAE,EAAE,WAAW,MAAM,CAAC;AAC1D,MAAI,OAAO,YAAY,SACrB,eAAc,KAAK,cAAc,SAAS,QAAQ;MAElD,eAAc,KAAK,cAAc,QAAQ;;CAI7C,WAAmB;EACjB,MAAM,UAAU,KAAK,SAAS,KAAK,MAAM,GAAG,KAAK,YAAY;AAC7D,SAAO,WAAW,SAAS,CAAC,OAAO,QAAQ,CAAC,OAAO,MAAM;;CAG3D,OAAe;AACb,SAAO,SAAS,KAAK,aAAa,CAAC;;CAGrC,IAAI,aAAsB;EAKxB,MAAM,QAAQ,KAAK,aAAa,MAAM,QAAQ;AAC9C,SAAO,MAAM,UAAU,KAAK,CAAC,kBAAkB,IAAI,MAAM,GAAI;;CAG/D,iBAA+B;AAC7B,MAAI,CAAC,KAAK,SAAU,QAAO,EAAE;EAE7B,MAAM,mBAAqC,KAAK,aAC5C,WACA;AAEJ,SAAO,mBAAmB,KAAK,MAAM,EAAE,EAAE,kBAAkB,CAAC;;;;;ACnGhE,MAAM,cAAc;AAOpB,IAAa,cAAb,MAAyB;CACvB;CAEA,YAAY,MAAc;AACxB,OAAK,WAAW,KAAK,MAAM,KAAK,MAAM,YAAY,CAAC;;CAGrD,OAAO,cAA+B;EACpC,IAAI,SAAS;AACb,OAAK,MAAM,EAAE,SAAS,aAAa,KAAK,SACtC,KAAI,KAAK,MAAM,SAAS,aAAa,CACnC,UAAS,CAAC;AAGd,SAAO;;CAGT,MAAc,UAA6B;AACzC,MAAI,CAAC,WAAW,SAAS,CAAE,QAAO,EAAE;AACpC,SAAO,aAAa,UAAU,QAAQ,CACnC,MAAM,KAAK,CACX,KAAK,MAAM,EAAE,MAAM,CAAC,CACpB,QAAQ,MAAM,KAAK,CAAC,EAAE,WAAW,IAAI,CAAC,CACtC,KAAK,MAAM;GACV,MAAM,UAAU,EAAE,WAAW,IAAI;GACjC,IAAI,UAAU,UAAU,EAAE,MAAM,EAAE,GAAG;AACrC,OAAI,QAAQ,WAAW,IAAI,CAAE,WAAU,QAAQ,MAAM,EAAE;AACvD,UAAO;IAAE;IAAS;IAAS;IAC3B;;CAGN,MAAc,SAAiB,MAAuB;AACpD,MAAI,QAAQ,SAAS,IAAI,CACvB,QAAO,KAAK,WAAW,QAAQ,IAAI,SAAS,QAAQ,MAAM,GAAG,GAAG;AAElE,MAAI,QAAQ,SAAS,IAAI,CACvB,QAAO,KAAK,QAAQ,SAAS,KAAK;AAEpC,SAAO,KAAK,QAAQ,SAAS,KAAK,IAAI,KAAK,QAAQ,SAAS,SAAS,KAAK,CAAC;;CAG7E,QAAgB,SAAiB,KAAsB;EACrD,MAAM,KAAK,QACR,MAAM,KAAK,CACX,KAAK,MACJ,EACG,QAAQ,qBAAqB,OAAO,CACpC,QAAQ,OAAO,QAAQ,CACvB,QAAQ,OAAO,OAAO,CAC1B,CACA,KAAK,KAAK;AACb,SAAO,IAAI,OAAO,IAAI,GAAG,GAAG,CAAC,KAAK,IAAI;;;;;ACvD1C,MAAM,gBAAgB;CAAC;CAAa;CAAU;CAAS;AACvD,MAAM,uBAAuB;AAE7B,IAAa,YAAb,MAAuB;CACrB;CACA;CAEA,YAAY,MAAc;AACxB,OAAK,OAAO,QAAQ,KAAK;AACzB,OAAK,SAAS,IAAI,YAAY,KAAK,KAAK;;CAG1C,UAAmB;AACjB,SACE,WAAW,KAAK,KAAK,MAAM,qBAAqB,CAAC,IACjD,cAAc,MAAM,MAAM;AACxB,OAAI;AACF,WAAO,SAAS,KAAK,KAAK,MAAM,EAAE,CAAC,CAAC,aAAa;WAC3C;AACN,WAAO;;IAET;;CAIN,QAAqB;AACnB,SAAO,KAAK,KAAK,KAAK,KAAK,CAAC,QACzB,MACC,mBAAmB,EAAE,aAAa,IAClC,CAAC,KAAK,OAAO,OAAO,EAAE,aAAa,CACtC;;CAGH,eAAe,YAAyC;AAEtD,SAAO,mBADM,KAAK,KAAK,WAAW,CACH,aAAa;;CAG9C,KAAK,YAA2C;AAC9C,MAAI,sBAAsB,UAAW,QAAO;AAI5C,SAAO,IAAI,UAHC,WAAW,WAAW,GAC9B,aACA,KAAK,KAAK,MAAM,WAAW,EACL,KAAK,KAAK;;CAGtC,KAAa,KAA0B;EACrC,MAAM,UAAuB,EAAE;AAC/B,OAAK,MAAM,SAAS,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC,EAAE;AAC7D,OAAI,MAAM,KAAK,WAAW,IAAI,CAAE;GAChC,MAAM,OAAO,KAAK,KAAK,MAAM,KAAK;AAClC,OAAI,MAAM,aAAa,EAAE;AACvB,QAAI,MAAM,SAAS,eAAgB;AACnC,YAAQ,KAAK,GAAG,KAAK,KAAK,KAAK,CAAC;cACvB,MAAM,QAAQ,CACvB,SAAQ,KAAK,IAAI,UAAU,MAAM,KAAK,KAAK,CAAC;;AAGhD,SAAO;;;;;AC9DX,IAAa,YAAb,MAAuB;CACrB,4BAAoB,IAAI,KAAqB;CAE7C,IAAI,KAA2B;AAC7B,MAAI,UAAU,KAAK;GACjB,gBAAgB;GAChB,iBAAiB;GACjB,YAAY;GACZ,+BAA+B;GAChC,CAAC;AACF,MAAI,MAAM,QAAQ;AAClB,OAAK,UAAU,IAAI,IAAI;AACvB,MAAI,GAAG,eAAe,KAAK,UAAU,OAAO,IAAI,CAAC;;CAGnD,UAAU,MAAoB;EAC5B,MAAM,UAAU,SAAS,KAAK;AAC9B,OAAK,MAAM,OAAO,KAAK,UACrB,KAAI;AACF,OAAI,MAAM,QAAQ;UACZ;AACN,QAAK,UAAU,OAAO,IAAI;;;CAKhC,QAAc;AACZ,OAAK,MAAM,OAAO,KAAK,UACrB,KAAI;AACF,OAAI,KAAK;UACH;AAIV,OAAK,UAAU,OAAO;;CAGxB,IAAI,OAAe;AACjB,SAAO,KAAK,UAAU;;;;;ACxC1B,SAAgB,qBAAqB,MAAmC;AACtE,QAAO;;;+BAGsB,KAAK,UAAU,EAAE,MAAM,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDxD,SAAgB,gBACd,MACA,MACQ;CACR,MAAM,SAAS,qBAAqB,KAAK;AACzC,KAAI,KAAK,SAAS,UAAU,CAC1B,QAAO,KAAK,QAAQ,WAAW,GAAG,OAAO,WAAW;AAEtD,QAAO,OAAO;;;;AC1DhB,MAAM,aAAa,IAAI,IAAI;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AASF,eAAsB,aACpB,KACA,KACA,MACe;CACf,MAAM,cAAc,GAAG,KAAK,QAAQ;CAEpC,MAAM,UAAkC,EAAE;AAC1C,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,IAAI,QAAQ,CAC9C,KAAI,CAAC,WAAW,IAAI,EAAE,aAAa,CAAC,IAAI,OAAO,MAAM,SACnD,SAAQ,KAAK;AAGjB,SAAQ,UAAU;AAClB,SAAQ,mBAAmB,OAAO,KAAK,QAAQ;AAC/C,SAAQ,gBAAgB;AACxB,SAAQ,qBAAqB;CAE7B,MAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,IAAI,QAAQ,OAAO;AACjE,KAAI,aAAa,IAAI,OAAO,IAAI;AAChC,KAAI,aAAa,IAAI,MAAM,IAAI;CAE/B,MAAM,UAAU,KAAK,gBAAgB,IAAI,EAAE;CAC3C,MAAM,QAAQ,IAAI,WAAW,SAAS,IAAI,WAAW;CACrD,IAAI,SAAS,IAAI,UAAU;CAC3B,IAAI;AAEJ,KAAI,QAAQ,SAAS,KAAK,OAAO;AAC/B,WAAS;EACT,MAAM,SAAS,IAAI,iBAAiB;AACpC,SAAO,IAAI,WAAW,IAAI,UAAU,MAAM;AAC1C,OAAK,MAAM,KAAK,QACd,QAAO,IAAI,qBAAqB,EAAE,aAAa,IAAI,EAAE,MAAM,CAAC;EAE9D,MAAM,QAAQ,cAAc;AAC5B,MAAI,MAAO,SAAQ,mBAAmB,UAAU;AAChD,UAAQ,kBAAkB;AAC1B,SAAO,OAAO,UAAU;AACxB,UAAQ,oBAAoB,OAAO,OAAO,WAAW,KAAK,CAAC;YAClD,CAAC,OAAO;AACjB,SAAO,MAAM,SAAS,IAAI;AAC1B,MAAI,KAAK,SAAS,EAChB,SAAQ,oBAAoB,OAAO,KAAK,OAAO;;AAInD,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,UAAgC;GACpC,UAAU;GACV,MAAM;GACN,MAAM,IAAI,YAAY,IAAI,UAAU;GACpC;GACA;GACD;EAED,MAAM,WAAW,MAAM,QAAQ,UAAU,aAAa;GAEpD,MAAM,UADc,SAAS,QAAQ,mBAAmB,IAC7B,SAAS,YAAY;GAEhD,MAAM,kBAAqD,EAAE;AAC7D,QAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,SAAS,QAAQ,CACnD,KAAI,CAAC,WAAW,IAAI,EAAE,aAAa,CAAC,IAAI,MAAM,KAAA,EAC5C,iBAAgB,KAAK;AAIzB,OAAI,QAAQ;IACV,MAAM,SAAmB,EAAE;AAC3B,aAAS,GAAG,SAAS,UAAkB,OAAO,KAAK,MAAM,CAAC;AAC1D,aAAS,GAAG,aAAa;KACvB,IAAI,OAAO,OAAO,OAAO,OAAO,CAAC,SAAS,QAAQ;AAClD,YAAO,gBAAgB,MAAM,KAAK,WAAW;AAC7C,qBAAgB,oBAAoB,OAAO,OAAO,WAAW,KAAK,CAAC;AACnE,SAAI,UAAU,SAAS,cAAc,KAAK,gBAAgB;AAC1D,SAAI,IAAI,KAAK;AACb,cAAS;MACT;UACG;AACL,QAAI,UAAU,SAAS,cAAc,KAAK,gBAAgB;AAC1D,aAAS,KAAK,IAAI;AAClB,aAAS,GAAG,OAAO,QAAQ;;IAE7B;AAEF,WAAS,GAAG,UAAU,QAAQ;AAC5B,UAAO,IAAI;IACX;AAEF,MAAI,KAAM,UAAS,MAAM,KAAK;AAC9B,WAAS,KAAK;GACd;;AAGJ,SAAS,SAAS,KAAuC;AACvD,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAmB,EAAE;AAC3B,MAAI,GAAG,SAAS,UAAkB,OAAO,KAAK,MAAM,CAAC;AACrD,MAAI,GAAG,aAAa,QAAQ,OAAO,OAAO,OAAO,CAAC,CAAC;AACnD,MAAI,GAAG,SAAS,OAAO;GACvB;;;;;;;;;;;ACmJJ,eAAsB,sBACpB,QACA,QAGA;AACA,QAAO,OAAO,IAAI,2BAA2B,OAAO;;;;;;;;;AAUtD,eAAsB,uBACpB,QACA,MAKA;AACA,QAAO,OAAO,KAAK,2BAA2B,KAAK;;;;;;;;;;AA4CrD,eAAsB,oBACpB,QACA,IACA,QAGA;AACA,QAAO,OAAO,IAAI,2BAA2B,MAAM,OAAO;;;;;;;;;;AA+C5D,eAAsB,uCACpB,QACA,IACA,QAGA;AACA,QAAO,OAAO,IACZ,2BAA2B,GAAG,wBAC9B,OACD;;;;;;;;;;AA2BH,eAAsB,oCACpB,QACA,IACA,MAKA;AACA,QAAO,OAAO,KACZ,2BAA2B,GAAG,yBAC9B,KACD;;;;;;;;;AA0BH,eAAsB,wBACpB,QACA,IAGA;AACA,QAAO,OAAO,KAAK,2BAA2B,GAAG,UAAU;;;;;;;;;AAU7D,eAAsB,eACpB,QACA,IAGA;AACA,QAAO,OAAO,IAAI,2BAA2B,GAAG,eAAe;;;;;;;;;AAcjE,eAAsB,mBACpB,QACA,sBAGA;AACA,QAAO,OAAO,IACZ,2BAA2B,qBAAqB,YACjD;;;;;;;;;;AAWH,eAAsB,oBACpB,QACA,sBACA,MAKA;AACA,QAAO,OAAO,IACZ,2BAA2B,qBAAqB,aAChD,KACD;;;;;;;;;;AAWH,eAAsB,oBACpB,QACA,sBACA,MAKA;AACA,QAAO,OAAO,OACZ,2BAA2B,qBAAqB,aAChD,EAAE,MAAM,CACT;;;;;;;;;AA8BH,eAAsB,mBACpB,QACA,MAKA;AACA,QAAO,OAAO,KAAK,uBAAuB,KAAK;;;;;;;;;AA8DjD,eAAsB,oBACpB,QACA,IAGA;AACA,QAAO,OAAO,OAAO,uBAAuB,KAAK;;;;ACjpBnD,MAAa,6BACX;AAOF,SAAS,yBAAyB,OAItB;AACV,KAAI,MAAM,WAAW,IAAK,QAAO;AACjC,KAAI,kCAAkC,KAAK,MAAM,QAAQ,CAAE,QAAO;AAClE,KAAI,MAAM,QAAQ,OAAO,MAAM,SAAS,UAAU;EAChD,MAAM,iBACJ,MAAM,KACN;AACF,MACE,kBACA,OAAO,eAAe,QAAQ,YAC9B,kCAAkC,KAAK,eAAe,IAAI,CAE1D,QAAO;;AAGX,QAAO;;;;;;;;;;;;;;AAeT,SAAgB,YAAY,GAAoB;AAC9C,KAAI,WAAW,EAAE,EAAE;EACjB,MAAM,SAAS,EAAE,SAAS,KAAK,EAAE,OAAO,KAAK;EAC7C,MAAM,OAAO,yBAAyB,EAAE,GACpC,4DAA4D,2BAA2B,8DACvF;AACJ,SAAO,GAAG,EAAE,UAAU,SAAS;;AAEjC,KAAI,aAAa,MAAO,QAAO,EAAE;AACjC,QAAO,OAAO,EAAE;;;;ACxClB,SAAS,kBAAkB,MAAiB,UAA0B;AACpE,QAAO,SAAS,KAAK,MAAM,SAAS,CAAC,MAAM,IAAI,CAAC,KAAK,IAAI;;AAG3D,SAAgB,WACd,MACA,SACqB;CACrB,MAAM,UAAU,SAAS,MAAM,KAAK,MAAM;EACxC,eAAe;EACf,UAAU,aAAqB;AAC7B,OAAI,SAAS,SAAS,eAAe,CAAE,QAAO;AAC9C,OAAI;IACF,MAAM,MAAM,kBAAkB,MAAM,SAAS;AAE7C,YADiB,IAAI,MAAM,QAAQ,CAAC,KAAK,IAAI,IAC7B,WAAW,IAAI,IAAI,KAAK,OAAO,OAAO,IAAI;WACpD;AACN,WAAO;;;EAGX,YAAY;EACZ,kBAAkB;GAAE,oBAAoB;GAAI,cAAc;GAAI;EAC/D,CAAC;CAEF,IAAI,UAAU,QAAQ,SAAS;CAC/B,MAAM,WAAW,OAA4B;AAM3C,YAAU,QAAQ,KAAK,GAAG,CAAC,OAAO,MAAM;AACtC,WAAQ,MAAM,uCAAuC,YAAY,EAAE,GAAG;IACtE;;AAGJ,SAAQ,GAAG,WAAW,aAAa;EACjC,MAAM,MAAM,kBAAkB,MAAM,SAAS;AAC7C,MAAI,CAAC,KAAK,eAAe,SAAS,IAAI,KAAK,OAAO,OAAO,IAAI,CAAE;EAC/D,MAAM,YAAY,KAAK,KAAK;AAC5B,gBAAc,QAAQ,CAAC,KAAK,KAAK,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,UAAU,CAAC;GAChE;AAEF,SAAQ,GAAG,QAAQ,aAAa;EAC9B,MAAM,MAAM,kBAAkB,MAAM,SAAS;AAC7C,MAAI,CAAC,KAAK,eAAe,SAAS,IAAI,KAAK,OAAO,OAAO,IAAI,CAAE;EAC/D,MAAM,YAAY,KAAK,KAAK;AAC5B,gBAAc,QAAQ,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,EAAE,EAAE,EAAE,UAAU,CAAC;GAChE;AAEF,SAAQ,GAAG,WAAW,aAAa;EACjC,MAAM,MAAM,kBAAkB,MAAM,SAAS;AAC7C,MAAI,CAAC,KAAK,eAAe,SAAS,IAAI,KAAK,OAAO,OAAO,IAAI,CAAE;EAC/D,MAAM,YAAY,KAAK,KAAK;AAC5B,gBAAc,QAAQ,EAAE,EAAE,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,EAAE,UAAU,CAAC;GAChE;AAEF,cAAa,QAAQ,OAAO;;;;ACtE9B,MAAM,WAAW;CAAC;CAAsB;CAAqB;CAAe;AAE5E,IAAa,6BAAb,cAAgD,MAAM;CACpD,cAAc;AACZ,QACE,yEACD;;;;;;;AAQL,IAAa,sBAAb,MAAiC;CAC/B;CACA;CAEA,YAAY,MAAc;AACxB,OAAK,OAAO,QAAQ,KAAK;AACzB,OAAK,WAAW,KAAK,UAAU;;CAGjC,kBAAwB;EACtB,MAAM,UAAU,KAAK,UAAU;AAC/B,OAAK,MAAM,OAAO,IAAI,IAAI,CAAC,GAAG,QAAQ,MAAM,EAAE,GAAG,KAAK,SAAS,MAAM,CAAC,CAAC,CACrE,MAAK,QAAQ,IAAI,IAAI,IAAI,WAAW,KAAK,SAAS,IAAI,IAAI,IAAI,MAC5D,OAAM,IAAI,4BAA4B;;CAK5C,OAAO,MAAyB,QAA0B;AACxD,OAAK,MAAM,OAAO,KAChB,KAAI,KAAK,YAAY,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,IAAI,MACvD,OAAM,IAAI,4BAA4B;AAG1C,UAAQ;AACR,OAAK,MAAM,OAAO,KAAM,MAAK,SAAS,IAAI,KAAK,KAAK,YAAY,IAAI,CAAC;;CAGvE,YAAoB,KAA4B;EAC9C,MAAM,OAAO,QAAQ,KAAK,MAAM,IAAI;AACpC,MAAI,CAAC,KAAK,WAAW,KAAK,OAAO,IAAI,CACnC,OAAM,IAAI,4BAA4B;AAExC,OAAK,IAAI,SAAS,OAAQ,SAAS,QAAQ,OAAO,EAAE;AAClD,OAAI;AACF,QAAI,UAAU,OAAO,CAAC,gBAAgB,CACpC,OAAM,IAAI,4BAA4B;YACjC,OAAO;AACd,QACE,EACE,OAAO,UAAU,YACjB,UAAU,QACV,UAAU,SACV,MAAM,SAAS,UAGjB,OAAM;;AAEV,OAAI,WAAW,KAAK,KAAM;;AAE5B,MAAI;AACF,UAAO,WAAW,SAAS,CAAC,OAAO,aAAa,KAAK,CAAC,CAAC,OAAO,MAAM;WAC7D,OAAO;AACd,OACE,OAAO,UAAU,YACjB,UAAU,QACV,UAAU,SACV,MAAM,SAAS,SAEf,QAAO;AACT,SAAM;;;CAIV,WAA+C;EAC7C,MAAM,yBAAS,IAAI,KAA4B;EAC/C,MAAM,SAAS,cAA4B;AACzC,QAAK,MAAM,SAAS,YAAY,WAAW,EAAE,eAAe,MAAM,CAAC,EAAE;AACnE,QAAI,MAAM,KAAK,WAAW,IAAI,IAAI,MAAM,SAAS,eAC/C;AACF,QAAI,MAAM,gBAAgB,CAAE,OAAM,IAAI,4BAA4B;IAClE,MAAM,OAAO,KAAK,WAAW,MAAM,KAAK;AACxC,QAAI,MAAM,aAAa,CAAE,OAAM,KAAK;SAC/B;KACH,MAAM,MAAM,SAAS,KAAK,MAAM,KAAK,CAAC,MAAM,IAAI,CAAC,KAAK,IAAI;AAC1D,YAAO,IAAI,KAAK,KAAK,YAAY,IAAI,CAAC;;;;AAI5C,QAAM,KAAK,KAAK;AAChB,OAAK,MAAM,OAAO,SAAU,QAAO,IAAI,KAAK,KAAK,YAAY,IAAI,CAAC;AAClE,SAAO;;;;;ACjGX,IAAa,qBAAb,cAAwC,MAAM;CAC5C,YAAY,YAAqD;AAC/D,QACE,4FAA4F,WACzF,KAAK,UAAU,KAAK,MAAM,KAAK,KAAK,GAAG,CACvC,KAAK,KAAK,GACd;AALkB,OAAA,aAAA;AAMnB,OAAK,OAAO;;;;AAKhB,SAAgB,uBAAuB,OAAgC;CACrE,MAAM,mCAAmB,IAAI,KAA0B;AACvD,MAAK,MAAM,WAAW,OAAO;EAC3B,MAAM,OAAO,0BAA0B,QAAQ;EAC/C,MAAM,YAAY,KAAK,aAAa;EACpC,MAAM,gBAAgB,iBAAiB,IAAI,UAAU,oBAAI,IAAI,KAAa;AAC1E,gBAAc,IAAI,KAAK;AACvB,mBAAiB,IAAI,WAAW,cAAc;;CAGhD,MAAM,aAAa,CAAC,GAAG,iBAAiB,QAAQ,CAAC,CAC9C,QAAQ,kBAAkB,cAAc,OAAO,EAAE,CACjD,KAAK,kBAAkB,CAAC,GAAG,cAAc,CAAC,MAAM,CAAC,CACjD,MAAM,CAAC,OAAO,KAAK,CAAC,QAAQ,QAAQ,KAAK,cAAc,MAAM,CAAC;AAEjE,KAAI,WAAW,SAAS,EAAG,OAAM,IAAI,mBAAmB,WAAW;;;;AClBrE,MAAM,gBAAgB;AACtB,MAAM,mBAAmB;;;;;AAMzB,MAAa,gCAAgC;;;;;;;;;AAoC7C,IAAa,qBAAb,MAAgC;CAC9B;CACA;CAEA,YAAY,WAAmB;AAC7B,OAAK,OAAO,KAAK,WAAW,cAAc;AAC1C,OAAK,SAAS,aAAa,KAAK,KAAK,CAAC;;CAGxC,SAAe;AACb,OAAK,SAAS,aAAa,KAAK,KAAK,CAAC;;CAGxC,OAAiB;AACf,SAAO,OAAO,KAAK,KAAK,OAAO;;CAGjC,UAA2C;AACzC,SAAO,OAAO,QAAQ,KAAK,OAAO,CAAC,KAAK,CAAC,KAAK,UAAU,CACtD,KACA,SAAS,KAAK,CACf,CAAC;;;;;;;CAQJ,YAAY,OAAqC,EAAE,EAAU;EAC3D,MAAM,UAAU,KAAK,SAAS,CAC3B,QAAQ,GAAG,UAAU,CAAC,KAAK,kBAAkB,CAAC,KAAK,QAAQ,CAC3D,UAAU,CAAC,OAAO,CAAC,WAClB,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI,EACxC;AACH,SAAO,WAAW,SAAS,CAAC,OAAO,KAAK,UAAU,QAAQ,CAAC,CAAC,OAAO,MAAM;;CAG3E,IAAI,KAAsB;AACxB,SAAO,KAAK,OAAO,SAAS,KAAA;;CAG9B,IAAI,KAAyC;EAC3C,MAAM,OAAO,KAAK,OAAO;AACzB,SAAO,OAAO,SAAS,KAAK,GAAG,KAAA;;CAGjC,IAAI,KAAa,MAA4B;AAC3C,MAAI,CAAC,gBAAgB,IAAI,IAAI,CAAC,iBAAiB,KAAK,CAClD,OAAM,IAAI,MAAM,2BAA2B,MAAM;AAEnD,OAAK,OAAO,OAAO,SAAS,KAAK;;CAGnC,OAAO,KAAmB;AACxB,SAAO,KAAK,OAAO;;CAGrB,QAAc;EACZ,MAAM,WAAuC;GAC3C,SAAS;GACT,QAAQ,WAAW,KAAK,OAAO;GAChC;EACD,MAAM,WAAW,GAAG,KAAK,KAAK,GAAG,YAAY,EAAE,CAAC,SAAS,MAAM,CAAC;AAEhE,MAAI;AACF,aAAU,QAAQ,KAAK,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AAClD,iBAAc,UAAU,KAAK,UAAU,UAAU,MAAM,EAAE,GAAG,MAAM;IAChE,UAAU;IACV,MAAM;IACP,CAAC;AACF,cAAW,UAAU,KAAK,KAAK;WACxB,OAAO;AACd,OAAI;AACF,eAAW,SAAS;WACd;AAGR,SAAM;;;;AAKZ,SAAS,aAAa,MAA0C;AAC9D,KAAI,CAAC,WAAW,KAAK,CAAE,QAAO,eAAe;AAE7C,KAAI;AACF,SAAOC,gBAAc,KAAK,MAAM,aAAa,MAAM,QAAQ,CAAC,CAAC;UACtD,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AACtE,QAAM,IAAI,MAAM,kBAAkB,cAAc,IAAI,UAAU;;;AAIlE,SAASA,gBAAc,OAA4C;AACjE,KAAI,CAACC,WAAS,MAAM,IAAI,MAAM,eAAe,iBAC3C,OAAM,IAAI,MAAM,oBAAoB,mBAAmB;CAGzD,MAAM,YAAY,MAAM;AACxB,KAAI,CAACA,WAAS,UAAU,CAAE,OAAM,IAAI,MAAM,4BAA4B;CAEtE,MAAM,SAAyC,EAAE;AACjD,MAAK,MAAM,CAAC,KAAK,YAAY,OAAO,QAAQ,UAAU,EAAE;AACtD,MAAI,CAAC,gBAAgB,IAAI,IAAI,CAAC,iBAAiB,QAAQ,CACrD,OAAM,IAAI,MAAM,2BAA2B,MAAM;AAEnD,SAAO,OAAO,SAAS,QAAQ;;AAGjC,QAAO;EAAE,SAAS;EAAkB;EAAQ;;AAG9C,SAAS,gBAA4C;AACnD,QAAO;EAAE,SAAS;EAAkB,QAAQ,EAAE;EAAE;;AAGlD,SAAS,WACP,QACgC;AAChC,QAAO,OAAO,YACZ,OAAO,QAAQ,OAAO,CAAC,KAAK,CAAC,KAAK,UAAU,CAAC,KAAK,SAAS,KAAK,CAAC,CAAC,CACnE;;AAGH,SAAS,SAAS,MAAsC;AACtD,QAAO;EACL,eAAe,KAAK;EACpB,GAAI,OAAO,KAAK,aAAa,WAAW,EAAE,UAAU,KAAK,UAAU,GAAG,EAAE;EACxE,GAAI,OAAO,KAAK,QAAQ,WAAW,EAAE,KAAK,KAAK,KAAK,GAAG,EAAE;EACzD,GAAI,OAAO,KAAK,gBAAgB,WAC5B,EAAE,aAAa,KAAK,aAAa,GACjC,EAAE;EACN,GAAI,OAAO,KAAK,gBAAgB,WAC5B,EAAE,aAAa,KAAK,aAAa,GACjC,EAAE;EACN,GAAI,OAAO,KAAK,oBAAoB,WAChC,EAAE,iBAAiB,KAAK,iBAAiB,GACzC,EAAE;EACN,GAAI,OAAO,KAAK,YAAY,WAAW,EAAE,SAAS,KAAK,SAAS,GAAG,EAAE;EACrE,GAAI,OAAO,KAAK,WAAW,WAAW,EAAE,QAAQ,KAAK,QAAQ,GAAG,EAAE;EAClE,GAAI,KAAK,YAAY,OAAO,EAAE,SAAS,MAAM,GAAG,EAAE;EAClD,GAAI,OAAO,KAAK,iBAAiB,WAC7B,EAAE,cAAc,KAAK,cAAc,GACnC,EAAE;EACP;;AAGH,SAAS,iBAAiB,OAAyC;AACjE,QACEA,WAAS,MAAM,IACf,OAAO,MAAM,qBAAqB,YAClC,OAAO,UAAU,MAAM,iBAAiB,IACxC,MAAM,mBAAmB,MACxB,MAAM,gBAAgB,KAAA,KACpB,OAAO,MAAM,gBAAgB,YAC5B,MAAM,YAAY,SAAS,OAC9B,MAAM,WAAW,KAAA,KACf,OAAO,MAAM,WAAW,YAAY,MAAM,OAAO,SAAS,OAC5D,MAAM,mBAAmB,KAAA,KACvB,OAAO,MAAM,mBAAmB,YAC/B,MAAM,eAAe,SAAS,OACjC,MAAM,mBAAmB,KAAA,KACvB,OAAO,MAAM,mBAAmB,YAC/B,OAAO,UAAU,MAAM,eAAe,IACtC,MAAM,iBAAiB,OAC1B,MAAM,uBAAuB,KAAA,KAC3B,OAAO,MAAM,uBAAuB,YACnC,MAAM,mBAAmB,SAAS,OACrC,MAAM,eAAe,KAAA,KAAa,OAAO,MAAM,eAAe,cAC9D,MAAM,cAAc,KAAA,KAClB,OAAO,MAAM,cAAc,YAAY,MAAM,UAAU,SAAS,OAClE,MAAM,eAAe,KAAA,KAAa,OAAO,MAAM,eAAe,eAC9D,MAAM,oBAAoB,KAAA,KACxB,OAAO,MAAM,oBAAoB,YAChC,MAAM,gBAAgB,SAAS;;AAIvC,SAAgB,gBAAgB,KAAsB;AACpD,KAAI,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,CAAE,QAAO;CAErD,MAAM,WAAW,IAAI,MAAM,IAAI;AAC/B,QACE,SAAS,OAAO,YAChB,SAAS,WAAW,KACpB,SAAS,OAAO,KAAA,KAChB,SAAS,GAAG,SAAS,KACrB,SAAS,OAAO,OAChB,SAAS,OAAO;;AAIpB,SAASA,WAAS,OAAkD;AAClE,QAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,MAAM;;;;AC3O7E,MAAM,yBACJ;AAEF,SAAgB,gBAAgB,KAAsB;AACpD,QAAO,uBAAuB,KAAK,IAAI;;;;ACgCzC,MAAM,8BAA8B;;;;;;;;;;AAoBpC,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YAAY,WAA0C;AACpD,QACE,YACI,iDAAiD,UAAU,qBAC3D,mCACL;AALyB,OAAA,YAAA;AAM1B,OAAK,OAAO;;;AAIhB,IAAa,SAAb,MAAoB;CAClB,gCAAwB,IAAI,KAAqB;CACjD,qCAA6B,IAAI,KAA6B;CAC9D,uCAA+B,IAAI,KAA+B;CAClE,sCAA8B,IAAI,KAA6B;CAC/D,qBAA6B;CAC7B,wBAAgC;CAChC,qBAA4C;CAC5C;CAEA,YACE,KACA,SACA,WACA,eACA;AAJQ,OAAA,MAAA;AACA,OAAA,UAAA;AACA,OAAA,YAAA;AAGR,OAAK,wBAAwB;;CAG/B,IAAY,gBAAoC;AAC9C,OAAK,0BAA0B,IAAI,mBAAmB,KAAK,UAAU,KAAK;AAC1E,SAAO,KAAK;;CAKd,MAAM,iBAAgC;EAKpC,MAAM,OAAQ,MAAMC,mBAClB,KAAK,KACL,KAAK,QACN;AAID,OAAK,gBAAgB,KAAK,+BAA+B,EAAE,CAAC;AAC5D,OAAK,qBAAqB,KAAK,uBAAuB;AACtD,OAAK,wBAAwB;;;;;;CAO/B,YAA2B;AACzB,SAAO,KAAK;;CAGd,gBAAwB,WAAmC;AACzD,yBACE,UAAU,SAAS,aAAc,SAAS,MAAM,CAAC,SAAS,IAAI,GAAG,EAAE,CAAE,CACtE;AACD,OAAK,mBAAmB,OAAO;AAC/B,OAAK,qBAAqB,OAAO;AACjC,OAAK,MAAM,YAAY,WAAW;AAChC,OAAI,CAAC,SAAS,IAAK;AAEnB,QAAK,mBAAmB,IAAI,SAAS,KAAK,SAAS;GACnD,MAAM,QAAQ,KAAK,qBAAqB,IAAI,SAAS,IAAI,IAAI,EAAE;AAC/D,SAAM,KAAK,SAAS;AACpB,QAAK,qBAAqB,IAAI,SAAS,KAAK,MAAM;;AAEpD,OAAK,qBAAqB;;CAG5B,kBAA0B,UAAgC;AACxD,MAAI,CAAC,SAAS,IAAK;AACnB,OAAK,mBAAmB,IAAI,SAAS,KAAK,SAAS;AACnD,OAAK,qBAAqB,IAAI,SAAS,KAAK,CAAC,SAAS,CAAC;AACvD,OAAK,qBAAqB;;CAG5B,qBAA6B,cAA4B;AACvD,OAAK,mBAAmB,OAAO,aAAa;AAC5C,OAAK,qBAAqB,OAAO,aAAa;AAC9C,OAAK,qBAAqB;;CAM5B,IAAY,YAAiC;AAC3C,MAAI,KAAK,mBAAoB,MAAK,sBAAsB;AACxD,SAAO,KAAK;;CAGd,IAAY,kBAA+C;AACzD,MAAI,KAAK,mBAAoB,MAAK,sBAAsB;AACxD,SAAO,KAAK;;CAGd,uBAAqC;AACnC,OAAK,qBAAqB;AAC1B,OAAK,cAAc,OAAO;AAC1B,OAAK,oBAAoB,OAAO;AAEhC,OAAK,MAAM,CAAC,KAAK,aAAa,KAAK,oBAAoB;AAGrD,OAAI,KAAK,mBAAmB,IAAI,GAAG,IAAI,SAAS,CAAE;AAElD,QAAK,oBAAoB,IAAI,KAAK,SAAS;AAC3C,OAAI,SAAS,SAAU,MAAK,cAAc,IAAI,KAAK,SAAS,SAAS;;;CAIzE,WAAW,MAA0B;AACnC,SAAO,KAAK,UAAU,KAAK,KAAK,UAAU,IAAI,KAAK,aAAa;;CAGlE,aAAuB;AACrB,SAAO,CAAC,GAAG,KAAK,gBAAgB,MAAM,CAAC;;;CAIzC,wBAAgC,KAAsB;EACpD,MAAM,WAAW,KAAK,gBAAgB,IAAI,IAAI;AAC9C,SAAO,UAAU,WAAW,QAAQ,uBAAuB,SAAS;;;CAItE,kBAA0C;AACxC,SAAO,OAAO,YAAY,KAAK,UAAU;;;CAI3C,kBAA0C;EACxC,MAAM,OAA+B,EAAE;AACvC,OAAK,MAAM,CAAC,KAAK,aAAa,KAAK,iBAAiB;AAClD,OAAI,CAAC,uBAAuB,SAAS,CAAE;GACvC,MAAM,MAAM,SAAS;AACrB,OAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,EAAG,MAAK,OAAO;;AAE7D,SAAO;;;CAIT,eAAe,kBAAiD;AAC9D,MAAI,CAAC,KAAK,yBAAyB,CAAC,KAAK,mBAAoB,QAAO;AAEpE,SAAO;GACL,SAAS,KAAK;GACd,WAAW,KAAK;GAChB;GACA,WAAW,CAAC,GAAG,KAAK,qBAAqB,QAAQ,CAAC,CAC/C,MAAM,CACN,IAAI,oBAAoB;GAC5B;;CAGH,kBAAkB,OAA6B;AAC7C,MAAI,MAAM,YAAY,KAAK,QACzB,OAAM,IAAI,MACR,sCAAsC,MAAM,QAAQ,SAAS,KAAK,UACnE;AAEH,OAAK,gBAAgB,MAAM,UAAU,IAAI,wBAAwB,CAAC;AAClE,OAAK,qBAAqB,MAAM;AAChC,OAAK,wBAAwB;;CAG/B,MAAc,8BAA6C;AACzD,MAAI,CAAC,KAAK,sBAAuB,OAAM,KAAK,gBAAgB;;;;;;CAO9D,MAAM,kBAAkB,OAA8B,EAAE,EAAmB;AACzE,OAAK,cAAc,QAAQ;AAC3B,QAAM,KAAK,6BAA6B;EAExC,MAAM,QAA4B,EAAE;AACpC,OAAK,MAAM,CAAC,KAAK,SAAS,KAAK,cAAc,SAAS,EAAE;AACtD,OAAI,KAAK,UAAU,OAAO,OAAO,IAAI,CAAE;AAIvC,OAAI,KAAK,UAAU,KAAK,IAAI,CAAC,OAAQ;GAErC,MAAM,iBACJ,KAAK,4BAA4B,KAAK,KAAK,IAC3C,KAAK,gBAAgB,IAAI,IAAI;AAC/B,OACE,kBACA,CAAC,KAAK,yBAAyB,gBAAgB,KAAK,CAEpD;AAEF,OAAI,kBAAkB,CAAC,KAAK,QAAS;GAErC,MAAM,WAAW,sBAAsB,KAAK;AAC5C,SAAM,KAAK;IACT;IACA;IACA,GAAI,iBAAiB,EAAE,gBAAgB,GAAG,EAAE;IAC5C,GAAI,WAAW,EAAE,UAAU,GAAG,EAAE;IACjC,CAAC;;AAGJ,MAAI,MAAM,SAAS,GAAG;AACpB,SAAM,KAAK,qBAAqB,MAAM;AACtC,SAAM,KAAK,sBAAsB,MAAM;AACvC,SAAM,KAAK,gBAAgB;;AAE7B,MAAI,KAAK,WAAY,MAAM,KAAK,sCAAsC,CACpE,OAAM,KAAK,gBAAgB;AAE7B,OAAK,gCAAgC;AAErC,SAAO,MAAM;;CAGf,MAAc,qBAAqB,OAA0C;EAC3E,MAAM,gCAAgB,IAAI,KAAiC;AAC3D,OAAK,MAAM,QAAQ,OAAO;AACxB,OAAI,KAAK,SAAU;GACnB,MAAM,cAAc,cAAc,IAAI,KAAK,KAAK,cAAc,IAAI,EAAE;AACpE,eAAY,KAAK,KAAK;AACtB,iBAAc,IAAI,KAAK,KAAK,eAAe,YAAY;;EAGzD,IAAI,kBAAkB;AACtB,OAAK,MAAM,CAAC,eAAe,gBAAgB,eAAe;GACxD,IAAI;AACJ,OAAI;AACF,mBAAe,MAAM,KAAK,wBAAwB,cAAc;YACzD,OAAO;AACd,UAAM,IAAI,MACR,6CAA6C,cAAc,IAAI,YAAY,MAAM,GAClF;;AAGH,QAAK,MAAM,QAAQ,aAAa;IAC9B,MAAM,iBAAiB,aAAa,IAAI,cAAc,KAAK,IAAI,CAAC;AAChE,QAAI,CAAC,eACH,OAAM,IAAI,MACR,sCAAsC,KAAK,IAAI,aAAa,gBAC7D;IAKH,MAAM,WAAgC;KACpC,GAAG;KACH,GAAI,OAAO,KAAK,KAAK,QAAQ,WAAW,EAAE,KAAK,KAAK,KAAK,KAAK,GAAG,EAAE;KACpE;AACD,SAAK,WAAW;AAChB,SAAK,cAAc,IAAI,KAAK,KAAK;KAC/B,GAAG,KAAK;KACR,GAAG;KACJ,CAAC;AACF,sBAAkB;;;AAItB,MAAI,gBAAiB,MAAK,cAAc,OAAO;;CAGjD,MAAc,wBACZ,eAC2C;EAC3C,MAAM,OAAO,MAAMC,eAAsB,KAAK,KAAK,cAAc;AACjE,MAAI,CAACC,WAAS,KAAK,IAAI,CAAC,MAAM,QAAQ,KAAK,kBAAkB,CAC3D,OAAM,IAAI,MAAM,uDAAuD;EAGzE,MAAM,yBAAS,IAAI,KAAkC;AACrD,OAAK,MAAM,SAAS,KAAK,mBAAmB;GAC1C,MAAM,QAAQ,wBAAwB,MAAM;AAC5C,OAAI,MAAO,QAAO,IAAI,MAAM,UAAU,MAAM,SAAS;;AAEvD,SAAO;;CAGT,MAAc,sBACZ,OACe;EACf,MAAM,SAAmB,EAAE;EAC3B,IAAI,WAAW;EAEf,MAAM,SAAS,YAAY;AACzB,UAAO,WAAW,MAAM,QAAQ;IAC9B,MAAM,OAAO,MAAM;AACnB,gBAAY;AACZ,QAAI,CAAC,QAAQ,CAAC,KAAK,SAAU;AAE7B,QAAI;AACF,WAAM,KAAK,qBAAqB,KAAK;aAC9B,OAAO;AACd,YAAO,KAAK,GAAG,KAAK,IAAI,IAAI,YAAY,MAAM,GAAG;;;;AAKvD,QAAM,QAAQ,IACZ,MAAM,KACJ,EAAE,QAAQ,KAAK,IAAI,6BAA6B,MAAM,OAAO,EAAE,EAC/D,OACD,CACF;AAED,MAAI,OAAO,SAAS,EAClB,OAAM,IAAI,MACR,kBAAkB,OAAO,OAAO,2EAA2E,OAAO,KAAK,KAAK,GAC7H;;CAIL,MAAc,qBAAqB,MAAuC;EACxE,MAAM,WAAW,KAAK;AACtB,MAAI,CAAC,SAAU,OAAM,IAAI,MAAM,kCAAkC;EAEjE,MAAM,mBAAmB,KAAK,gBAAgB;AAC9C,MAAI,KAAK,kBAAkB,OAAO,qBAAqB,SACrD,OAAM,IAAI,MAAM,2CAA2C;EAqB7D,MAAM,oBAAoB,sBAlBb,MAAMC,mBAA0B,KAAK,KAAK,EACrD,eAAe;GACb,KAAK,SAAS;GACd,UAAU,cAAc,KAAK,IAAI;GACjC,cAAc,SAAS;GACvB,cAAc,SAAS;GACvB,GAAI,SAAS,kBACT,EAAE,mBAAmB,SAAS,iBAAiB,GAC/C,EAAE;GACN,GAAI,SAAS,YAAY,KAAA,IACrB,EAAE,UAAU,SAAS,SAAS,GAC9B,EAAE;GACN,GAAI,SAAS,SAAS,EAAE,QAAQ,SAAS,QAAQ,GAAG,EAAE;GACtD,eAAe,KAAK;GACpB,iBAAiB;GAClB,EACF,CAAC,CAEmD;AACrD,MAAI,CAAC,kBACH,OAAM,IAAI,MAAM,oDAAoD;AAGtE,MAAI,OAAO,qBAAqB,SAAU;AAE1C,MAAI;AACF,SAAMC,oBAA2B,KAAK,KAAK,iBAAiB;WACrD,OAAO;AACd,OAAI,gBAAgB,MAAM,CAAE;AAC5B,OAAI;AACF,UAAMA,oBAA2B,KAAK,KAAK,kBAAkB;WACvD;AAGR,SAAM;;;;;;;CAQV,uBAA+B,KAA+B;AAC5D,MAAI,KAAK,mBAAmB,IAAI,GAAG,IAAI,SAAS,CAAE,QAAO,EAAE;AAC3D,SAAO,KAAK,qBAAqB,IAAI,IAAI,IAAI,EAAE;;CAGjD,4BACE,KACA,MAC4B;AAC5B,SAAO,KAAK,uBAAuB,IAAI,CAAC,MACrC,aACC,uBAAuB,SAAS,IAChC,CAAC,KAAK,yBAAyB,UAAU,KAAK,CACjD;;;CAIH,MAAc,uCAAyD;EACrE,MAAM,oBAAgE,EAAE;AAExE,OAAK,MAAM,CAAC,KAAK,SAAS,KAAK,cAAc,SAAS,EAAE;AACtD,OAAI,KAAK,UAAU,OAAO,OAAO,IAAI,IAAI,CAAC,KAAK,IAAK;GAEpD,MAAM,YAAY,KAAK,uBAAuB,IAAI,CAAC,OACjD,uBACD;AACD,OAAI,UAAU,SAAS,EAAG;GAE1B,MAAM,oBAAoB,UAAU,QACjC,aAAa,SAAS,QAAQ,KAAK,IACrC;AACD,OAAI,kBAAkB,WAAW,EAAG;GAEpC,MAAM,SAAS,KAAK,iBAAiB,mBAAmB,IAAI;GAC5D,MAAM,WAAW,KAAK,4BAA4B,QAAQ,IAAI;AAC9D,QAAK,MAAM,YAAY,WAAW;IAChC,MAAM,aAAa,KAAK,4BAA4B,UAAU,IAAI;AAClE,QAAI,eAAe,SACjB,mBAAkB,KAAK;KAAE;KAAK;KAAY,CAAC;;;AAIjD,MAAI,kBAAkB,WAAW,EAAG,QAAO;EAE3C,MAAM,SAAmB,EAAE;EAC3B,IAAI,eAAe;EACnB,MAAM,SAAS,YAAY;AACzB,UAAO,eAAe,kBAAkB,QAAQ;IAC9C,MAAM,WAAW,kBAAkB;AACnC,oBAAgB;AAChB,QAAI,CAAC,SAAU;AAEf,QAAI;AACF,WAAMA,oBAA2B,KAAK,KAAK,SAAS,WAAW;aACxD,OAAO;AACd,SAAI,CAAC,gBAAgB,MAAM,CACzB,QAAO,KAAK,GAAG,SAAS,IAAI,IAAI,YAAY,MAAM,GAAG;;;;AAM7D,QAAM,QAAQ,IACZ,MAAM,KACJ,EACE,QAAQ,KAAK,IACX,6BACA,kBAAkB,OACnB,EACF,EACD,OACD,CACF;AAED,MAAI,OAAO,SAAS,EAClB,OAAM,IAAI,MACR,oBAAoB,OAAO,OAAO,wCAAwC,OAAO,KAAK,KAAK,GAC5F;AAGH,SAAO;;CAGT,iBACE,WACA,KACgB;EAChB,IAAI,SAAS,UAAU;AACvB,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,gCAAgC,MAAM;EAEnE,IAAI,WAAW,KAAK,4BAA4B,QAAQ,IAAI;AAC5D,OAAK,MAAM,YAAY,UAAU,MAAM,EAAE,EAAE;GACzC,MAAM,aAAa,KAAK,4BAA4B,UAAU,IAAI;AAClE,OAAI,aAAa,UAAU;AACzB,aAAS;AACT,eAAW;;;AAGf,SAAO;;CAGT,4BACE,UACA,KACQ;EACR,MAAM,aAAa,gBAAgB,SAAS,YAAY;AACxD,MAAI,CAAC,WACH,OAAM,IAAI,MAAM,6CAA6C,MAAM;AAErE,SAAO;;;CAIT,qCAA2C;AACzC,OAAK,cAAc,QAAQ;EAC3B,IAAI,UAAU;AAEd,OAAK,MAAM,CAAC,KAAK,SAAS,KAAK,cAAc,SAAS,EAAE;AACtD,OAAI,KAAK,UAAU,OAAO,OAAO,IAAI,CAAE;AACvC,OAAI,KAAK,UAAU,KAAK,IAAI,CAAC,OAAQ;GAErC,MAAM,WAAW,KAAK,gBAAgB,IAAI,IAAI;AAC9C,OAAI,CAAC,uBAAuB,SAAS,CACnC,OAAM,IAAI,MAAM,+CAA+C,MAAM;GAGvE,MAAM,WAA2B;IAAE,GAAG;IAAM,eAAe,KAAK;IAAS;AACzE,UAAO,SAAS;AAChB,UAAO,SAAS;AAChB,UAAO,OAAO,UAAU,aAAa,SAAS,CAAC;AAC/C,QAAK,cAAc,IAAI,KAAK,SAAS;AACrC,aAAU;;AAGZ,MAAI,QAAS,MAAK,cAAc,OAAO;;;;;;;;CAWzC,MAAM,WACJ,MACA,SACA,OAAmC,EAAE,EACb;AACxB,MAAI,KAAK,QAAQ;GACf,MAAM,UAAU,KAAK,MAAM;GAC3B,MAAM,WAAW,MAAM,KAAK,YAC1B;IAAE,KAAK,KAAK;IAAc;IAAS,EACnC,QACD;AACD,QAAK,kBAAkB;IACrB,GAAG;IACH,KAAK,KAAK;IACV;IACA,UAAU,SAAS,YAAY,KAAK,UAAU;IAC/C,CAAC;AACF,UAAO;;AAGT,MAAI,yBAAyB,KAAK,CAChC,OAAM,IAAI,MACR,kDAAkD,KAAK,eACxD;EAGH,MAAM,WAAW,MAAM,KAAK,iBAAiB,MAAM,QAAQ;AAC3D,OAAK,kBAAkB,SAAS;AAChC,MAAI,gBAAgB,KAAK,aAAa,CACpC,MAAK,sBAAsB,MAAM,UAAU,KAAK,aAAa;AAE/D,SAAO;;;;;;;;;;;;;;;;;;;;;;;;CAyBT,MAAc,YACZ,UACA,SACyB;EACzB,MAAM,OAAgC,EACpC,4BAA4B,UAC7B;AACD,MAAI,QAAS,MAAK,cAAc;AAEhC,MAAI;GACF,MAAM,WAAY,MAAMC,oBACtB,KAAK,KACL,KAAK,SACL,KACD;AAID,OAAI,SAAS,oBACX,MAAK,qBAAqB,SAAS;AAErC,UACE,SAAS,8BAA8B;IACrC,KAAK,OAAO,SAAS,WAAW,WAAW,SAAS,SAAS;IAC7D,UAAU;IACX;WAEI,GAAG;AACV,SAAM,KAAK,kBAAkB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCnC,MAAM,cAAgC;AACpC,MAAI;GACF,MAAM,WAAY,MAAM,KAAK,IAAI,KAC/B,2BAA2B,KAAK,QAAQ,kBACxC,EAAE,CACH;AACD,OAAI,SAAS,oBACX,MAAK,qBAAqB,SAAS;AAErC,UAAO;WACA,GAAG;AACV,WAAQ,KACN,+CAA+C,YAAY,EAAE,CAAC,wCAC/D;AACD,UAAO;;;CAIX,MAAM,cAAc,SAAmD;AACrE,MAAI,CAAC,QAAS;AAEd,MAAI;GACF,MAAM,WAAY,MAAM,KAAK,IAAI,KAC/B,2BAA2B,KAAK,QAAQ,wBACxC,EAAE,UAAU,SAAS,CACtB;AACD,OAAI,SAAS,WAAY,MAAK,qBAAqB,SAAS;WACrD,GAAG;AACV,SAAM,KAAK,kBAAkB,EAAE;;;CAInC,kBAA0B,GAAqB;AAI7C,OAFG,GAA2D,UAC3D,GAA0C,UAAU,YACxC,IAAK,QAAO;EAM3B,MAAM,OAAQ,GAAqD,MAC/D;AACJ,SAAO,IAAI,kBAAkB,MAAM,cAAc,KAAK;;CAGxD,MAAc,iBACZ,MACA,SACiC;EAWjC,MAAM,SATkB,MAAM,KAAK,IAAI,KAEpC,mBAAmB,EACpB,mBAAmB;GACjB,aAAa,2BAA2B,KAAK;GAC7C,WAAW,KAAK,KAAK;GACrB,MAAM,KAAK;GACZ,EACF,CAAC,EAC4B;EAG9B,MAAM,WAAW,MAAM,KAAK,IAAI,KAI7B,iCAAiC,EAAE,CAAC;EAGvC,MAAM,SAAS,KAAK,8BAA8B,MAAM,eAAe;EACvE,MAAM,WAAW,IAAI,UAAU;EAC/B,MAAM,OAAO,IAAI,KAAK,CAAC,KAAK,YAAY,CAA2B,EAAE,EACnE,MAAM,KAAK,KAAK,MACjB,CAAC;AACF,WAAS,OAAO,QAAQ,MAAM,KAAK,KAAK;AACxC,WAAS,OAAO,SAAS,SAAS,MAAM;AACxC,WAAS,OAAO,aAAa,SAAS,UAAU;AAChD,WAAS,OAAO,UAAU,OAAO,SAAS,OAAO,CAAC;AAClD,WAAS,OAAO,UAAU,OAAO;AACjC,WAAS,OAAO,YAAY,KAAK,KAAK;AACtC,WAAS,OAAO,aAAa,sCAAsC;EAEnE,MAAM,SAAS,MAAM,MACnB,kDACA;GACE,QAAQ;GACR,MAAM;GACP,CACF;AACD,MAAI,CAAC,OAAO,GAAI,OAAM,IAAI,MAAM,2BAA2B,OAAO,SAAS;EAC3E,MAAM,SAAU,MAAM,OAAO,MAAM;EAUnC,MAAM,kBAA2C,EAC/C,OAAO;GACL,IAAI,MAAM;GACV,kBAAkB,OAAO;GACzB,cAAc,OAAO;GACrB,WAAW,KAAK,KAAK;GACrB,MAAM,KAAK;GACX,WAAW,OAAO;GAClB,eAAe,MAAM;GACtB,EACF;AACD,MAAI,OAAO,OACR,iBAAgB,SAAqC,YACpD,OAAO;AACX,MAAI,OAAO,MACR,iBAAgB,SAAqC,WACpD,OAAO;EAEX,MAAM,eAAe,MAAM,KAAK,IAAI,KAEjC,qCAAqC,gBAAgB;EAMxD,MAAM,SAAS,MAAM,KAAK,YACxB;GACE,KAAK,KAAK;GACV,WAAW;IACT,gBAAgB,aAAa,MAAM;IACnC,cAAc,KAAK,KAAK;IACxB,cAAc,OAAO;IACrB,UAAU,KAAK;IACf,QAAQ,aAAa,MAAM;IAC3B,KAAK,aAAa,MAAM;IACxB,mBAAmB,OAAO;IAC3B;GACF,EACD,QACD;AAID,SAAO;GACL,GAAG;GACH,KAAK,OAAO,OAAO,KAAK;GACxB,KAAK,OAAO,OAAO,aAAa,MAAM;GACtC,cAAc,aAAa,MAAM;GACjC,eAAe;IACb,KAAK,aAAa,MAAM;IACxB,aAAa,KAAK,KAAK;IACvB,aAAa,OAAO;IACpB,iBAAiB,OAAO;IACxB,SAAS,KAAK;IACd,QAAQ,aAAa,MAAM;IAC5B;GACF;;CAGH,sBACE,MACA,UACA,SACM;AAGN,OAAK,cAAc,QAAQ;AAC3B,OAAK,cAAc,IAAI,KAAK,cAAc;GACxC,eAAe,KAAK;GACpB,GAAG,aAAa,SAAS;GACzB,GAAG,sBAAsB,MAAM,SAAS;GACxC,GAAI,UAAU,EAAE,SAAS,MAAM,GAAG,EAAE;GACpC,GAAI,OAAO,SAAS,iBAAiB,WACjC,EAAE,cAAc,SAAS,cAAc,GACvC,EAAE;GACP,CAAC;AACF,OAAK,cAAc,OAAO;AAC1B,aAAW,KAAK,aAAa;;CAG/B,8BAAsC,eAA+B;EACnE,MAAM,QAAQ,cAAc,MAAM,IAAI;EACtC,MAAM,YAAY,MAAM,MAAM;EAC9B,MAAM,WAAW,MAAM,MAAM;EAC7B,MAAM,YAAY,MAAM,MAAM;AAQ9B,SAAO,GAAG,UAAU,GAPsB;GACxC,QAAQ;GACR,QAAQ;GACR,OAAO;GACP,WAAW;GACX,OAAO;GACR,CACgC,aAAa,QAAQ,GAAG;;CAK3D,MAAM,iBACJ,cACA,SACe;AAGf,OAAK,cAAc,QAAQ;AAC3B,MAAI,KAAK,cAAc,IAAI,aAAa,CAAE;EAE1C,MAAM,OAAgC,EACpC,4BAA4B,EAAE,KAAK,cAAc,EAClD;AACD,MAAI,QAAS,MAAK,cAAc;AAEhC,MAAI;GACF,MAAM,WAAY,MAAMC,oBACtB,KAAK,KACL,KAAK,SACL,KACD;AACD,OAAI,SAAS,oBACX,MAAK,qBAAqB,SAAS;WAE9B,GAAG;AACV,OAAI,CAAC,gBAAgB,EAAE,CAAE,OAAM,KAAK,kBAAkB,EAAE;;AAE1D,OAAK,qBAAqB,aAAa;;CAKzC,MAAM,cAAyC;EAI7C,MAAM,OAAQ,MAAMN,mBAClB,KAAK,KACL,KAAK,QACN;EAID,MAAM,YAAY,KAAK,+BAA+B,EAAE;AACxD,OAAK,gBAAgB,UAAU;AAC/B,OAAK,qBAAqB,KAAK,uBAAuB;AACtD,OAAK,wBAAwB;AAC7B,SAAO;;CAGT,MAAM,oBAAoB,KAA8B;EACtD,MAAM,OAAO,MAAM,MAAM,IAAI;AAC7B,MAAI,CAAC,KAAK,GAAI,OAAM,IAAI,MAAM,6BAA6B,KAAK,SAAS;AACzE,SAAO,OAAO,KAAK,MAAM,KAAK,aAAa,CAAC;;;;;;;;CAS9C,MAAM,wBACJ,WACA,MACyE;AACzE,OAAK,cAAc,QAAQ;EAE3B,MAAM,8BAAc,IAAI,KAAa;EACrC,MAAM,sCAAsB,IAAI,KAAa;EAC7C,MAAM,oCAAoB,IAAI,KAAa;EAC3C,MAAM,wCAAwB,IAAI,KAAa;EAC/C,MAAM,gCAAgB,IAAI,KAAwB;EAClD,MAAM,SAAmB,EAAE;EAC3B,IAAI,kBAAkB;EAMtB,MAAM,2BAA2B,UAAU,QAAQ,aAAa;GAC9D,MAAM,MAAM,SAAS;AACrB,OAAI,CAAC,IAAK,QAAO;GAEjB,MAAM,OAAO,KAAK,UAAU,KAAK,IAAI;AACrC,OAAI,CAAC,KAAK,gBAAgB,KAAK,KAAK,CAAE,QAAO;AAC7C,OAAI,CAAC,yBAAyB,UAAU,KAAK,KAAK,CAAE,QAAO;GAE3D,MAAM,WAAW,KAAK,cAAc,IAAI,IAAI;AAC5C,OAAI,KAAK,UAAU,OAAO,OAAO,IAAI,IAAI,UAAU,QAAS,QAAO;AACnE,UACE,CAAC,YACD,SAAS,QAAQ,SAAS,OAC1B,CAAC,sBAAsB,SAAS;IAElC;EACF,IAAI,gCAAgB,IAAI,KAAkC;EAC1D,MAAM,yCAAyB,IAAI,KAAa;AAChD,MAAI,yBAAyB,SAAS,GAAG;AACvC,OAAI;AACF,oBAAgB,MAAM,KAAK,wBAAwB,KAAK,QAAQ;YACzD,OAAO;AACd,QAAI,iBAAiB,2BAA4B,OAAM;AACvD,WAAO,KAAK,+BAA+B,YAAY,MAAM,GAAG;AAChE,SAAK,MAAM,YAAY,yBACrB,KAAI,SAAS,IAAK,wBAAuB,IAAI,SAAS,IAAI;;AAI9D,OAAI,uBAAuB,SAAS,EAClC,MAAK,MAAM,YAAY,0BAA0B;IAC/C,MAAM,MAAM,SAAS;AACrB,QAAI,CAAC,OAAO,cAAc,IAAI,cAAc,IAAI,CAAC,CAAE;AACnD,2BAAuB,IAAI,IAAI;AAC/B,WAAO,KACL,sCAAsC,IAAI,aAAa,KAAK,UAC7D;;;AAKP,OAAK,iBAAiB,iBAAiB;AACvC,OAAK,MAAM,YAAY,WAAW;GAChC,MAAM,MAAM,SAAS;AACrB,OAAI,CAAC,IAAK;GAEV,MAAM,OAAO,KAAK,UAAU,KAAK,IAAI;AACrC,OAAI,CAAC,KAAK,gBAAgB,KAAK,KAAK,CAAE;AACtC,OAAI,CAAC,yBAAyB,UAAU,KAAK,KAAK,CAAE;AAEpD,qBAAkB,IAAI,IAAI;AAC1B,OAAI,KAAK,UAAU,OAAO,OAAO,IAAI,EAAE;AACrC,QAAI,KAAK,cAAc,IAAI,IAAI,CAAE,uBAAsB,IAAI,IAAI;AAC/D,gBAAY,IAAI,IAAI;AACpB;;AAMF,OAAI,KAAK,cAAc,IAAI,IAAI,EAAE,SAAS;AACxC,0BAAsB,IAAI,IAAI;AAC9B,gBAAY,IAAI,IAAI;AACpB;;AAMF,OAAI,uBAAuB,IAAI,IAAI,EAAE;IAKnC,MAAM,QAAQ,KAAK,cAAc,IAAI,IAAI;AACzC,QAAI,SAAS,MAAM,QAAQ,SAAS,KAAK;AACvC,UAAK,cAAc,OAAO,IAAI;AAC9B,uBAAkB;;AAEpB;;AAGF,OAAI;IACF,MAAM,WAAW,KAAK,cAAc,IAAI,IAAI;IAC5C,MAAM,WAAW,cAAc,IAAI,cAAc,IAAI,CAAC;AACtD,SAAK,cAAc,IAAI,KAAK;KAC1B,GAAG;KACH,eAAe,KAAK;KACpB,GAAG;KACH,GAAG,aAAa,SAAS;KAC1B,CAAC;AACF,sBAAkB;AAClB,wBAAoB,IAAI,IAAI;AAC5B,gBAAY,IAAI,IAAI;AACpB,kBAAc,IAAI,KAAK,KAAK;YACrB,OAAO;AACd,QAAI,iBAAiB,2BAA4B,OAAM;AACvD,WAAO,KAAK,eAAe,IAAI,IAAI,YAAY,MAAM,GAAG;;;AAI5D,MAAI,KAAK,OACP,MAAK,MAAM,CAAC,KAAK,SAAS,KAAK,cAAc,SAAS,EAAE;AACtD,OACE,kBAAkB,IAAI,IAAI,IAC1B,sBAAsB,IAAI,IAAI,IAC9B,KAAK,UAAU,OAAO,OAAO,IAAI,IACjC,KAAK,QAEL;AAEF,QAAK,cAAc,OAAO,IAAI;AAC9B,qBAAkB;;AAItB,MAAI,gBACF,KAAI;AAGF,OAAI,KAAK,gBACP,MAAK,gBAAgB,OAAO,CAAC,qBAAqB,QAChD,KAAK,cAAc,OAAO,CAC3B;OACE,MAAK,cAAc,OAAO;WACxB,OAAO;AACd,OAAI,iBAAiB,2BAA4B,OAAM;AACvD,UAAO,KAAK,kCAAkC,YAAY,MAAM,GAAG;AACnE,QAAK,MAAM,OAAO,oBAAqB,aAAY,OAAO,IAAI;AAC9D,UAAO;IAAE;IAAa,QAAQ;IAAG;IAAQ;;EAI7C,IAAI,SAAS;AACb,OAAK,MAAM,CAAC,KAAK,SAAS,cACxB,KAAI;AACF,OAAI,KAAK,OACP,KAAI,KAAK,gBACP,MAAK,gBAAgB,OAAO,CAAC,IAAI,QAC/B,WAAW,KAAK,aAAa,CAC9B;OACE,YAAW,KAAK,aAAa;AAEpC;WACO,OAAO;AACd,OAAI,iBAAiB,2BAA4B,OAAM;AACvD,UAAO,KAAK,eAAe,IAAI,IAAI,YAAY,MAAM,GAAG;;AAM5D,SAAO;GAAE;GAAa;GAAQ;GAAQ;;CAGxC,gBAAwB,KAAa,MAA0B;AAC7D,SACE,CAAC,IAAI,SAAS,KAAK,IACnB,CAAC,IAAI,MAAM,QAAQ,CAAC,SAAS,KAAK,KACjC,KAAK,iBAAiB,KAAK,UAAU,QACpC,KAAK,aAAa,WAAW,KAAK,UAAU,OAAO,IAAI;;CAI7D,yBACE,UACA,MACS;AACT,MAAI,CAAC,uBAAuB,SAAS,CAAE,QAAO;AAC9C,MAAI,KAAK,QAAQ,KAAA,EAAW,QAAO,SAAS,QAAQ,KAAK;AACzD,MAAI,KAAK,aAAa,KAAA,EAAW,QAAO,SAAS,aAAa,KAAK;AACnE,SAAO;;CAGT,iCAA+C;EAC7C,MAAM,aAAa,KAAK,cACrB,SAAS,CACT,QACE,CAAC,SACA,CAAC,KAAK,UAAU,OAAO,OAAO,IAAI,IAClC,CAAC,KAAK,UAAU,KAAK,IAAI,CAAC,OAC7B,CACA,KAAK,CAAC,SAAS,IAAI,CACnB,QAAQ,QAAQ,CAAC,uBAAuB,KAAK,gBAAgB,IAAI,IAAI,CAAC,CAAC;AAC1E,MAAI,WAAW,SAAS,EACtB,OAAM,IAAI,MACR,yCAAyC,WAAW,KAAK,KAAK,GAC/D;;CAML,MAAM,YACJ,OAeI,EAAE,EACe;EACrB,MAAM,aAAa,KAAK,UAAU,OAAO;AACzC,yBAAuB,WAAW,KAAK,SAAS,KAAK,aAAa,CAAC;EACnE,MAAM,SAAqB;GACzB,UAAU;GACV,SAAS;GACT,YAAY;GACZ,QAAQ;GACR,QAAQ,EAAE;GACV,kBAAkB;GACnB;AAGD,MAAI,KAAK,UAAU;AACjB,QAAK,MAAM,QAAQ,YAAY;AAC7B,QAAI,CAAC,KAAK,SAAU;IAEpB,MAAM,SADc,KAAK,gBAAgB,CACd,QAAQ,MAAM,EAAE,aAAa,QAAQ;AAChE,SAAK,MAAM,KAAK,OACd,QAAO,OAAO,KAAK,GAAG,KAAK,aAAa,IAAI,EAAE,UAAU;;AAG5D,OAAI,OAAO,OAAO,SAAS,GAAG;AAC5B,WAAO,mBAAmB;AAC1B,WAAO;;;AAIX,MAAI,KAAK,YACP,MAAK,kBAAkB,KAAK,YAAY;MAExC,OAAM,KAAK,gBAAgB;AAM7B,MAAI,CAAC,KAAK,cAAe,OAAM,KAAK,cAAc,KAAK,QAAQ;EAO/D,IAAI,UAAU,KAAK,WAAW;AAE9B,MAAI,KAAK,mBAAmB;AAC1B,UAAO,SAAS,MAAM,KAAK,kBAAkB,KAAK,kBAAkB;AAIpE,aAAU,KAAK,sBAAsB;aAC5B,KAAK,uBAAuB;AACrC,QAAK,cAAc,QAAQ;AAC3B,QAAK,gCAAgC;;EAGvC,MAAM,WAAW,WAAW,QAAQ,MAAM,EAAE,UAAU,KAAK,WAAW,EAAE,CAAC;EACzE,IAAI,OAAO;AACX,OAAK,MAAM,QAAQ,UAAU;AAC3B,OAAI;AACF,UAAM,KAAK,WAAW,MAAM,SAAS,EACnC,cAAc,KAAK,qBACpB,CAAC;AAIF,cAAU,KAAK;AACf,WAAO;YACA,GAAG;AACV,QAAI,aAAa,kBAAmB,OAAM;AAC1C,WAAO,OAAO,KAAK,UAAU,KAAK,aAAa,IAAI,YAAY,EAAE,GAAG;;AAEtE,QAAK,aAAa,EAAE,MAAM,SAAS,OAAO;;AAG5C,MAAI,KAAK,QAAQ;GACf,MAAM,aAAa,IAAI,IAAI,WAAW,KAAK,MAAM,EAAE,aAAa,CAAC;AACjE,QAAK,MAAM,OAAO,KAAK,cAAc,MAAM,CAAE,YAAW,IAAI,IAAI;GAChE,MAAM,WAAW,KAAK,YAAY,CAAC,QAChC,QACC,KAAK,wBAAwB,IAAI,IACjC,CAAC,WAAW,IAAI,IAAI,IACpB,CAAC,KAAK,UAAU,OAAO,OAAO,IAAI,CACrC;AACD,QAAK,MAAM,OAAO,SAChB,KAAI;AACF,UAAM,KAAK,iBAAiB,KAAK,QAAQ;AACzC,cAAU,KAAK;AACf,WAAO;YACA,GAAG;AACV,QAAI,aAAa,kBAAmB,OAAM;AAC1C,WAAO,OAAO,KAAK,UAAU,IAAI,IAAI,YAAY,EAAE,GAAG;;;AAK5D,SAAO;;CAKT,MAAM,cACJ,OAII,EAAE,EACqC;EAC3C,MAAM,YAAY,MAAM,KAAK,aAAa;EAC1C,MAAM,qBAAqB,MAAM,KAAK,wBAAwB,WAAW,EACvE,QAAQ,KAAK,UAAU,OACxB,CAAC;EACF,MAAM,SAA2C;GAC/C,UAAU;GACV,SAAS;GACT,YAAY;GACZ,QAAQ,mBAAmB;GAC3B,SAAS;GACT,QAAQ,CAAC,GAAG,mBAAmB,OAAO;GACtC,kBAAkB;GACnB;EAED,IAAI,OAAO;AACX,OAAK,MAAM,YAAY,WAAW;AAChC,OAAI,mBAAmB,YAAY,IAAI,SAAS,IAAI,EAAE;AACpD,SAAK,aAAa,EAAE,MAAM,UAAU,OAAO;AAC3C;;AAEF,OAAI,KAAK,MAAM,IAAI,SAAS,IAAI,EAAE;AAChC,WAAO;AACP,SAAK,aAAa,EAAE,MAAM,UAAU,OAAO;AAC3C;;GAGF,MAAM,OAAO,KAAK,UAAU,KAAK,SAAS,IAAI;AAG9C,OAAI,CAAC,KAAK,gBAAgB,SAAS,KAAK,KAAK,EAAE;AAC7C,WAAO,OAAO,KAAK,YAAY,SAAS,IAAI,2BAA2B;AACvE,SAAK,aAAa,EAAE,MAAM,UAAU,OAAO;AAC3C;;AAGF,OAAI;AACF,QAAI,SAAS,kBAAkB,kBAAkB,SAAS,KAAK;KAC7D,MAAM,MAAM,MAAM,KAAK,oBAAoB,SAAS,IAAI;AACxD,UAAK,MAAM,IAAI;eAEf,SAAS,YAAY,KAAA,KACrB,SAAS,YAAY,MACrB;KACA,MAAM,UACJ,OAAO,SAAS,YAAY,WACxB,SAAS,UACT,KAAK,UAAU,SAAS,QAAQ;AACtC,UAAK,MAAM,QAAQ;;AAErB,WAAO;YACA,GAAG;AACV,WAAO,OAAO,KAAK,YAAY,SAAS,IAAI,IAAI,YAAY,EAAE,GAAG;;AAEnE,QAAK,aAAa,EAAE,MAAM,UAAU,OAAO;;AAG7C,MAAI,KAAK,QAAQ;GACf,MAAM,aAAa,IAAI,IAAI,UAAU,KAAK,MAAM,EAAE,IAAI,CAAC;AACvD,QAAK,MAAM,QAAQ,KAAK,UAAU,OAAO,EAAE;AACzC,QAAI,WAAW,IAAI,KAAK,aAAa,CAAE;AAOvC,QAAI,gBAAgB,KAAK,aAAa,CAAE;AAExC,QAAI;AACF,gBAAW,KAAK,aAAa;AAC7B,YAAO;YACD;;;AAMZ,SAAO;;;AAIX,SAAS,yBACP,UACA,KACA,MACS;AACT,QACE,gBAAgB,IAAI,IAAI,uBAAuB,SAAS,IAAI,CAAC,KAAK;;AAItE,SAAS,yBAAyB,MAA0B;AAC1D,QACE,CAAC,KAAK,UACN,KAAK,aAAa,WAAW,UAAU,IACvC,CAAC,gBAAgB,KAAK,aAAa;;AAIvC,SAAS,uBACP,UAC4B;AAC5B,QACE,UAAU,kBAAkB,kBAC5B,OAAO,SAAS,QAAQ,YACxB,SAAS,IAAI,SAAS;;AAI1B,SAAS,wBAAwB,OAA4C;AAC3E,QAAO;EACL,KAAK,MAAM;EACX,UAAU,MAAM;EAChB,SAAS,MAAM,iBAAiB,KAAK;EACrC,eAAe,MAAM;EACrB,aAAa,MAAM;EACnB,KAAK,MAAM;EACZ;;AAGH,SAAS,oBAAoB,UAA+C;AAC1E,QAAO;EACL,KAAK,SAAS;EACd,UAAU,SAAS;EACnB,gBAAgB,SAAS,WAAW;EACpC,cAAc,SAAS;EACvB,YAAY,SAAS;EACrB,KAAK,SAAS;EACf;;AAGH,SAAS,gBAAgB,OAAyB;AAChD,QAAO,WAAW,MAAM,IAAI,MAAM,WAAW;;AAG/C,SAAS,aACP,UAC0C;AAC1C,QAAO;EACL,GAAI,OAAO,SAAS,aAAa,YAAY,SAAS,SAAS,SAAS,IACpE,EAAE,UAAU,SAAS,UAAU,GAC/B,EAAE;EACN,GAAI,OAAO,SAAS,QAAQ,YAAY,SAAS,IAAI,SAAS,IAC1D,EAAE,KAAK,SAAS,KAAK,GACrB,EAAE;EACP;;AAGH,SAAS,cAAc,KAAqB;AAC1C,QAAO,IAAI,MAAM,EAAiB;;AAGpC,SAAS,sBACP,MACiC;AACjC,KACE,OAAO,KAAK,QAAQ,YACpB,KAAK,IAAI,WAAW,KACpB,OAAO,KAAK,gBAAgB,YAC5B,KAAK,YAAY,WAAW,KAC5B,OAAO,KAAK,gBAAgB,YAC5B,CAAC,OAAO,UAAU,KAAK,YAAY,IACnC,KAAK,eAAe,EAEpB;AAGF,QAAO;EACL,KAAK,KAAK;EACV,aAAa,KAAK;EAClB,aAAa,KAAK;EAClB,GAAI,OAAO,KAAK,oBAAoB,WAChC,EAAE,iBAAiB,KAAK,iBAAiB,GACzC,EAAE;EACN,GAAI,OAAO,KAAK,YAAY,WAAW,EAAE,SAAS,KAAK,SAAS,GAAG,EAAE;EACrE,GAAI,OAAO,KAAK,WAAW,WAAW,EAAE,QAAQ,KAAK,QAAQ,GAAG,EAAE;EACnE;;AAGH,SAAS,sBACP,MACA,UACqB;AACrB,KAAI,SAAS,cAAe,QAAO,SAAS;AAC5C,KAAI,CAAC,SAAS,IACZ,OAAM,IAAI,MAAM,8BAA8B,KAAK,eAAe;AAGpE,QAAO;EACL,KAAK,SAAS;EACd,aAAa,KAAK,KAAK;EACvB,aAAa,KAAK,MAAM;EACxB,SAAS,KAAK;EACd,GAAI,OAAO,SAAS,iBAAiB,WACjC,EAAE,QAAQ,SAAS,cAAc,GACjC,EAAE;EACP;;AAGH,SAAS,wBACP,OACiE;AACjE,KAAI,CAACE,WAAS,MAAM,CAAE,QAAO,KAAA;CAE7B,MAAM,WAAW,eAAe,MAAM,YAAY;CAClD,MAAM,MAAM,eAAe,MAAM,OAAO;CACxC,MAAM,cAAc,eAAe,MAAM,gBAAgB;CACzD,MAAM,cAAc,gBAAgB,MAAM,gBAAgB;AAC1D,KAAI,CAAC,YAAY,CAAC,OAAO,CAAC,eAAe,CAAC,YAAa,QAAO,KAAA;CAE9D,MAAM,kBAAkB,eAAe,MAAM,qBAAqB;CAClE,MAAM,UAAU,eAAe,MAAM,YAAY;CACjD,MAAM,SAAS,eAAe,MAAM,UAAU;AAC9C,QAAO;EACL;EACA,UAAU;GACR;GACA;GACA;GACA,GAAI,kBAAkB,EAAE,iBAAiB,GAAG,EAAE;GAC9C,GAAI,YAAY,KAAA,IAAY,EAAE,SAAS,GAAG,EAAE;GAC5C,GAAI,SAAS,EAAE,QAAQ,GAAG,EAAE;GAC7B;EACF;;AAGH,SAAS,sBAAsB,OAAoC;AACjE,KAAI,CAACA,WAAS,MAAM,IAAI,CAACA,WAAS,MAAM,iBAAiB,CACvD;AAEF,QAAO,gBAAgB,MAAM,iBAAiB,MAAM;;AAGtD,SAAS,gBAAgB,OAAoC;AAC3D,KAAI,OAAO,UAAU,YAAY,OAAO,UAAU,MAAM,IAAI,QAAQ,EAClE,QAAO;AAET,KAAI,OAAO,UAAU,YAAY,CAAC,QAAQ,KAAK,MAAM,CAAE,QAAO,KAAA;CAE9D,MAAM,SAAS,OAAO,MAAM;AAC5B,QAAO,OAAO,cAAc,OAAO,IAAI,SAAS,IAAI,SAAS,KAAA;;AAG/D,SAAS,eAAe,OAAoC;AAC1D,QAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ,KAAA;;AAGjE,SAAS,eAAe,OAAoC;AAC1D,QAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;;AAG7C,SAASA,WAAS,OAAkD;AAClE,QAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,MAAM;;;;;;;;;;;;;;;;;;;;;;;;ACvgD7E,SAAgB,8BAA8B,SAA0B;CACtE,IAAI,eAAe;CACnB,IAAI,kBAAkB;AAEtB,MAAK,MAAM,SAAS,QAAQ,SAAS,qBAAqB,CACxD,SAAQ,MAAM,IAAd;EACE,KAAK;AACH,mBAAgB;AAChB;EACF,KAAK;AACH,OAAI,eAAe,EAAG,iBAAgB;AACtC;EACF,KAAK;AACH,sBAAmB;AACnB;EACF,KAAK;AACH,OAAI,kBAAkB,EAAG,oBAAmB;AAC5C;;AAIN,QAAO,eAAe,KAAK,kBAAkB;;AAQ/C,MAAM,aAAa,IAAI,IAAI;CACzB,CAAC,WAAW,aAAa;CACzB,CAAC,QAAQ,UAAU;CACnB,CAAC,WAAW,aAAa;CACzB,CAAC,OAAO,SAAS;CACjB,CAAC,QAAQ,UAAU;CACnB,CAAC,MAAM,QAAQ;CACf,CAAC,aAAa,eAAe;CAC7B,CAAC,cAAc,gBAAgB;CAC/B,CAAC,YAAY,cAAc;CAC3B,CAAC,OAAO,SAAS;CACjB,CAAC,UAAU,YAAY;CACvB,CAAC,SAAS,WAAW;CACrB,CAAC,cAAc,gBAAgB;CAC/B,CAAC,YAAY,cAAc;CAC3B,CAAC,UAAU,YAAY;CACxB,CAAC;AAEF,MAAM,eAAe,IAAI,IAAI,WAAW,QAAQ,CAAC;AACjD,MAAM,oBAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;;;;AAkBF,SAAgB,8BACd,SAC4B;CAC5B,MAAM,QAAqB,EAAE;CAC7B,MAAM,cAA0C,EAAE;CAClD,IAAI,OAAO;CACX,IAAI,mBAAmB;CAEvB,MAAM,cAAc,MAAc,YAA0B;EAC1D,MAAM,OAAO,MAAM,GAAG,GAAG;AAEzB,MAAI,QAAQ,kBAAkB,IAAI,KAAK,KAAK,EAAE;AAC5C,OAAI,SAAS,KAAK,cAAe,OAAM,KAAK;AAC5C;;EAGF,MAAM,gBAAgB,WAAW,IAAI,KAAK;AAC1C,MAAI,eAAe;AACjB,SAAM,KAAK;IAAE;IAAM;IAAe,MAAM;IAAS,CAAC;AAClD;;AAGF,MAAI,CAAC,aAAa,IAAI,KAAK,CAAE;AAE7B,MAAI,CAAC,MAAM;AACT,eAAY,KAAK;IACf,UAAU;IACV,SAAS,6BAA6B,KAAK,eAAe,QAAQ;IACnE,CAAC;AACF;;AAGF,MAAI,SAAS,KAAK,eAAe;AAC/B,eAAY,KAAK;IACf,UAAU;IACV,SAAS,6BAA6B,KAAK,eAAe,QAAQ,QAAQ,KAAK,KAAK,iBAAiB,KAAK,KAAK,uBAAuB,KAAK,cAAc;IAC1J,CAAC;AACF;;AAGF,QAAM,KAAK;;AAGb,MAAK,MAAM,SAAS,QAAQ,SAC1B,uDACD,EAAE;EACD,MAAM,OAAO,MAAM,IAAI,aAAa;AACpC,MAAI,CAAC,KAAM;EAEX,MAAM,QAAQ,MAAM,SAAS;AAG7B,OAAK,IAAI,SAAS,kBAAkB,SAAS,OAAO,SAClD,KAAI,QAAQ,WAAW,OAAO,KAAK,GAAI,SAAQ;AAEjD,qBAAmB;EAEnB,MAAM,OAAO,MAAM,GAAG,GAAG;AACzB,MAAI,SAAS,YAAY,EAAE,QAAQ,kBAAkB,IAAI,KAAK,KAAK,GAAG;GAIpE,MAAM,aAAa,MAAM,GACtB,QAAQ,sBAAsB,GAAG,CACjC,QAAQ,UAAU,GAAG,CACrB,MAAM,KAAK;AACd,QAAK,MAAM,CAAC,QAAQ,cAAc,WAAW,SAAS,EAAE;IACtD,MAAM,gBAAgB,0BAA0B,KAAK,UAAU,GAAG;AAClE,QAAI,CAAC,cAAe;AACpB,eAAW,cAAc,aAAa,EAAE,OAAO,OAAO;;QAGxD,YAAW,MAAM,KAAK;;AAI1B,MAAK,MAAM,QAAQ,MAAM,SAAS,CAChC,aAAY,KAAK;EACf,UAAU;EACV,SAAS,2BAA2B,KAAK,KAAK,eAAe,KAAK,KAAK,iBAAiB,KAAK,cAAc;EAC5G,CAAC;AAGJ,QAAO;;;;;;;;;;;;;ACrKT,IAAa,iBAAb,cAAoC,MAAM;CACxC,YACE,MACA,MACA;AACA,QAAM,0BAA0B,MAAM,KAAK,CAAC;AAH5B,OAAA,OAAA;AACA,OAAA,OAAA;AAGhB,OAAK,OAAO;;;AAIhB,SAAgB,0BAA0B,MAAc,MAAsB;AAC5E,QACE,QAAQ,KAAK,MAAM,KAAK;;;;;;;AAW5B,SAAgB,mBAAmB,MAAc,MAA6B;AAC5E,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAS,IAAI,cAAc;AAEjC,SAAO,KAAK,UAAU,QAA+B;AACnD,OAAI,IAAI,SAAS,gBAAgB,IAAI,SAAS,SAC5C,QAAO,IAAI,eAAe,MAAM,KAAK,CAAC;OAEtC,QAAO,IAAI;IAEb;AAEF,SAAO,KAAK,mBAAmB;AAC7B,UAAO,YAAY,SAAS,CAAC;IAC7B;AAEF,SAAO,OAAO,MAAM,KAAK;GACzB;;;;AClCJ,SAAS,YAAoB;AAC3B,yBAAO,IAAI,MAAM,EAAC,mBAAmB,SAAS,EAAE,QAAQ,OAAO,CAAC;;AAsBlE,eAAsB,eACpB,KACA,OACA,WACA,MACA,SACqB;CACrB,MAAM,MAAM,IAAI,WAAW;CAC3B,MAAM,SAAS,IAAI,OAAO,KAAK,MAAM,IAAI,UAAU;CACnD,IAAI,kBAAkB;CACtB,MAAM,8BAA8B;AAClC,MAAI,CAAC,gBAAiB;AACtB,oBAAkB;AAClB,MAAI;AACF,QAAK,4BAA4B;UAC3B;;CAIV,MAAM,0BAA0B;AAC9B,MAAI,CAAC,gBAAiB;AACtB,MAAI,KAAK,eAAe;GACtB,MAAM,QAAQ,OAAO,eACnB,IAAI,mBAAmB,UAAU,KAAK,CAAC,aAAa,CACrD;AACD,OAAI,MACF,KAAI;AACF,SAAK,cAAc,MAAM;WACnB;AACN,2BAAuB;;;;CAM/B,MAAM,iCAAiB,IAAI,KAAa;AAGxC,SAAQ,IAAI,mBAAmB,MAAM,KAAK,KAAK,MAAM,GAAG,IAAI;CAC5D,MAAM,YAAY,MAAc,UAAkB;AAChD,UAAQ,OAAO,MAAM,iBAAiB,KAAK,GAAG,MAAM,SAAS;;CAE/D,MAAM,8BACJ,OAAO,YAAY;EACjB,QAAQ;EACR,UAAU,KAAK;EACf,mBAAmB,EAAE,SAAS,MAAM;EACpC,qBAAqB;EACrB,YAAY;EACb,CAAC;CACJ,IAAI;AACJ,KAAI,KAAK,aAAa;EACpB,IAAI,uBAAuB;AAC3B,MAAI;AACF,UAAO,kBAAkB,KAAK,YAAY;AAC1C,SAAM,OAAO,cAAc,KAAK,YAAY,UAAU;AACtD,0BAAuB;WAChB,OAAO;AACd,OAAI,EAAE,iBAAiB,mBAAoB,OAAM;;AAGnD,eAAa,uBACT,MAAM,OAAO,YAAY;GACvB,QAAQ;GACR,UAAU,KAAK;GACf,mBAAmB,EAAE,SAAS,MAAM;GACpC,qBAAqB;GACrB,SAAS,OAAO,WAAW;GAC3B,aAAa,KAAK;GAClB,eAAe;GACf,YAAY;GACb,CAAC,GACF,MAAM,uBAAuB;OAEjC,cAAa,MAAM,uBAAuB;AAE5C,SAAQ,OAAO,MAAM,KAAK;AAC1B,KAAI,WAAW,SAAS,EACtB,SAAQ,IAAI,WAAW,WAAW,OAAO,6BAA6B;AAExE,KAAI,WAAW,kBAAkB;AAC/B,UAAQ,MACN,+BAA+B,WAAW,OAAO,OAAO,oCACzD;AACD,OAAK,MAAM,KAAK,WAAW,OAAQ,SAAQ,MAAM,KAAK,IAAI;AAC1D,UAAQ,KAAK,EAAE;YACN,WAAW,OAAO,SAAS,GAAG;AACvC,yBAAuB;AACvB,OAAK,MAAM,KAAK,WAAW,OAAQ,SAAQ,MAAM,KAAK,IAAI;AAC1D,MAAI,WAAW,WAAW,WAAW,YAAY,EAAG,SAAQ,KAAK,EAAE;;AAErE,KAAI,WAAW,OAAO,WAAW,EAC/B,oBAAmB;CA8BrB,MAAM,eAAe;CACrB,IAAI,gBAAgB;CACpB,IAAI,cAAoD;CACxD,IAAI,eAA8B,QAAQ,SAAS;CAQnD,IAAI,UAAU;CACd,IAAI,sBAAsB;CAC1B,MAAM,qBAAqB,UAAmC;AAC5D,MAAI,oBAAqB;AACzB,wBAAsB;AACtB,yBAAuB;AACvB,UAAQ,MACN,8DAA8D,MAAM,QAAQ,gFAC7E;;CAEH,MAAM,iBAAuB;AAC3B,iBAAe,aAAa,KAAK,YAAY;GAC3C,MAAM,WAAW,MAAM,OAAO,aAAa;AAC3C,aAAU,CAAC;AACX,OAAI,SAAU,oBAAmB;IACjC;;CAEJ,MAAM,qBAA2B;AAC/B,MAAI,CAAC,YAAa;AAClB,eAAa,YAAY;AACzB,gBAAc;AACd,YAAU;;CAEZ,MAAM,qBAA2B;AAC/B,MAAI,YAAa,cAAa,YAAY;AAC1C,gBAAc,iBAAiB;AAC7B,iBAAc;AACd,aAAU;KACT,aAAa;;CAGlB,MAAM,cAAc,WAClB,WACA,OAAO,UAAU,OAAO,SAAS,cAAc;AAC7C,MAAI,YAAY,gBAAgB,aAE9B,eAAc;WACL,aAAa;AAEtB,gBAAa,YAAY;AACzB,iBAAc;;AAEhB,kBAAgB;AAGhB,QAAM;AAGN,MAAI,SAAS;AACX,aAAU;AACV,SAAM;;AAER,MAAI,oBAAqB;AAEzB,MAAI;AACF,0BACE,UAAU,OAAO,CAAC,KAAK,SAAS,KAAK,aAAa,CACnD;WACM,OAAO;AACd,WAAQ,MAAM,6BAA6B,OAAO,MAAM,GAAG;AAC3D;;EAGF,MAAM,UAAU,CAAC,GAAG,UAAU,GAAG,MAAM;EACvC,IAAI,cAAc;AAElB,OAAK,MAAM,QAAQ,SAAS;AAE1B,OAAI,KAAK,YAAY,KAAK,UAAU;IAClC,MAAM,cAAc,KAAK,gBAAgB;AACzC,SAAK,MAAM,KAAK,aAAa;KAC3B,MAAM,SACJ,EAAE,aAAa,UAAU,iBAAiB;AAC5C,aAAQ,KAAK,MAAM,OAAO,IAAI,KAAK,aAAa,IAAI,EAAE,UAAU;;;AAIpE,kBAAe,IAAI,KAAK,aAAa;AACrC,OAAI;IAKF,MAAM,kBAAkB,MAAM,OAAO,WACnC,MACA,OAAO,WAAW,EAClB,EAAE,cAAc,MAAM,CACvB;AACD,kBAAc;AACd,YAAQ,IAAI,cAAc,KAAK,aAAa,IAAI,WAAW,CAAC,GAAG;AAO/D,QAAI,gBAAgB,KAAK,aAAa,CACpC,SAAQ,KACN,OAAO,KAAK,aAAa,oIAC1B;AAQH,QACE,KAAK,YACL,oBAAoB,QACpB,8BAA8B,gBAAgB,CAE9C,SAAQ,KACN,OAAO,KAAK,aAAa,mGAC1B;AAEH,QAAI,KAAK,YAAY,oBAAoB,KACvC,MAAK,MAAM,cAAc,8BACvB,gBACD,CACC,SAAQ,KAAK,OAAO,KAAK,aAAa,IAAI,WAAW,UAAU;YAG5D,GAAG;AACV,QAAI,aAAa,mBAAmB;AAClC,uBAAkB,EAAE;AACpB;;AAEF,2BAAuB;AACvB,YAAQ,MACN,8BAA8B,KAAK,aAAa,IAAI,IACrD;aACO;AACR,mBAAe,OAAO,KAAK,aAAa;;;AAI5C,MAAI,oBAAqB;AAEzB,OAAK,MAAM,QAAQ,SAAS;AAC1B,OAAI,UAAU,OAAO,OAAO,KAAK,aAAa,CAAE;AAChD,OAAI;AACF,UAAM,OAAO,iBAAiB,KAAK,cAAc,OAAO,WAAW,CAAC;AACpE,kBAAc;AACd,YAAQ,IAAI,eAAe,KAAK,eAAe;YACxC,OAAO;AACd,QAAI,iBAAiB,mBAAmB;AACtC,uBAAkB,MAAM;AACxB;;AAEF,2BAAuB;;;AAI3B,MAAI,oBAAqB;AAEzB,MAAI,YAAa,oBAAmB;AAEpC,MAAI,QAAQ,SAAS,EACnB,KAAI,UAAU,KAAK,UAAU,EAAE,aAAa,MAAM,CAAC,CAAC;WAC3C,QAAQ,SAAS,EAC1B,KAAI,UACF,KAAK,UAAU,EAAE,UAAU,QAAQ,KAAK,MAAM,EAAE,aAAa,EAAE,CAAC,CACjE;AAGH,gBAAc;GAEjB;CAGD,MAAM,SAAS,KAAK,aAAa,OAAO,KAAK,QAAQ;AACnD,MAAI,IAAI,QAAQ,eAAe;AAC7B,OAAI,IAAI,IAAI;AACZ;;AAGF,MAAI;AACF,SAAM,aAAa,KAAK,KAAK;IAC3B,SAAS,MAAM;IACf,SAAS,MAAM;IACf,YAAY,KAAK;IACjB,oBACE,CAAC,GAAG,eAAe,CAChB,KAAK,MAAM,UAAU,KAAK,EAAE,CAAC,CAC7B,QAAQ,MAAM,EAAE,OAAO,CACvB,KAAK,OAAO;KACX,cAAc,EAAE;KAChB,YAAY,EAAE,MAAM;KACrB,EAAE;IACR,CAAC;WACK,GAAG;AACV,WAAQ,MAAM,WAAW,IAAI,OAAO,GAAG,IAAI,IAAI,KAAK,IAAI;AACxD,OAAI,CAAC,IAAI,aAAa;IAUpB,MAAM,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;AAC1D,QAAI,UAAU,KAAK,EAAE,gBAAgB,6BAA6B,CAAC;AACnE,QAAI,IACF,mDAAmD,MAAM,QAAQ,cAAc,QAAQ,0SAIxF;;;GAGL;AAOF,OAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,SAAO,KAAK,UAAU,QAA+B;AACnD,OAAI,IAAI,SAAS,gBAAgB,IAAI,SAAS,UAAU;AACtD,YAAQ,MAAM,0BAA0B,KAAK,MAAM,KAAK,KAAK,CAAC;AAC9D,YAAQ,KAAK,EAAE;;AAEjB,UAAO,IAAI;IACX;AACF,SAAO,OAAO,KAAK,MAAM,KAAK,YAAY,SAAS,CAAC;GACpD;CAEF,MAAM,UAAU,UAAU,KAAK,KAAK,GAAG,KAAK;AAC5C,WAAU,QAAQ;AAGlB,QAAO,SAAS,OAAO;AACrB,MAAI,OAAO;AACX,eAAa;AACb,SAAO,OAAO;;;;;AC1YlB,MAAM,mBAAmB;AACzB,MAAM,gBAAgB,KAAK,gBAAgB,oBAAoB;AAkB/D,SAAgB,sBACd,WACA,SACA,kBACuB;AACvB,KAAI;EAIF,MAAM,QAAQ,cAHC,KAAK,MAClB,aAAa,KAAK,WAAW,cAAc,EAAE,QAAQ,CACtD,CACkC;AACnC,MAAI,MAAM,YAAY,QAAS,QAAO;AACtC,MAAI,MAAM,qBAAqB,iBAAkB,QAAO;AACxD,SAAO;SACD;AACN,SAAO;;;AAIX,SAAgB,uBACd,WACA,OACM;CACN,MAAM,WAAW;EAAE,SAAS;EAAkB,GAAG;EAAO;AACxD,eAAc,SAAS;CAEvB,MAAM,OAAO,KAAK,WAAW,cAAc;CAC3C,MAAM,WAAW,GAAG,KAAK,GAAG,YAAY,EAAE,CAAC,SAAS,MAAM,CAAC;AAC3D,KAAI;AACF,YAAU,QAAQ,KAAK,EAAE,EAAE,WAAW,MAAM,CAAC;AAC7C,gBAAc,UAAU,GAAG,KAAK,UAAU,SAAS,CAAC,KAAK;GACvD,UAAU;GACV,MAAM;GACP,CAAC;AACF,aAAW,UAAU,KAAK;UACnB,OAAO;AACd,SAAO,UAAU,EAAE,OAAO,MAAM,CAAC;AACjC,QAAM;;;AAIV,SAAgB,wBAAwB,WAAyB;AAC/D,QAAO,KAAK,WAAW,cAAc,EAAE,EAAE,OAAO,MAAM,CAAC;;AAGzD,eAAsB,+BACpB,WACA,QACA,SACA,WACgC;AAChC,KAAI;AACF,MAAI,CAAE,MAAM,OAAO,SAAS,CAAG,QAAO;EAEtC,MAAM,gBAAgB,IAAI,mBAAmB,UAAU,KAAK;EAC5D,MAAM,YAAmC,EAAE;AAC3C,OAAK,MAAM,OAAO,MAAM,OAAO,WAAW,EAAE;GAC1C,MAAM,UAAU,MAAM,OAAO,WAAW,IAAI;AAC5C,OAAI,CAAC,QAAS,QAAO;GACrB,MAAM,QAAQ,cAAc,IAAI,IAAI;AACpC,OAAI,OAAO;AACT,QACE,MAAM,WACN,CAAC,MAAM,OACP,CAAC,QAAQ,OAAO,OAAO,KAAA,wBAAmC,CAAC,CAE3D,QAAO;AAET,cAAU,KAAK;KACb;KACA,UAAU,MAAM,YAAY;KAC5B,gBAAgB;KAChB,cAAc;KACd,KAAK,MAAM;KACZ,CAAC;AACF;;AAGF,OAAI,QAAQ,OAAO,OAAO,KAAA,wBAAmC,CAAC,CAC5D,QAAO;AACT,aAAU,KAAK;IACb;IACA,UAAU,WAAW,SAAS,CAAC,OAAO,QAAQ,CAAC,OAAO,MAAM;IAC5D,gBAAgB;IACjB,CAAC;;AAGJ,SAAO;GACL;GACA;GACA,kBAAkB,cAAc,aAAa;GAC7C;GACD;SACK;AACN,SAAO;;;AAIX,SAAS,cAAc,UAAmC;AACxD,KAAI,CAAC,SAAS,SAAS,IAAI,SAAS,eAAe,iBACjD,OAAM,IAAI,MAAM,oBAAoB,mBAAmB;CAGzD,MAAM,UAAU,SAAS;CACzB,MAAM,YAAY,SAAS;CAC3B,MAAM,mBAAmB,SAAS;CAClC,MAAM,eAAe,SAAS;AAC9B,KAAI,CAAC,kBAAkB,QAAQ,CAAE,OAAM,IAAI,MAAM,mBAAmB;AACpE,KAAI,CAAC,iBAAiB,UAAU,CAAE,OAAM,IAAI,MAAM,qBAAqB;AACvE,KAAI,CAAC,iBAAiB,iBAAiB,CACrC,OAAM,IAAI,MAAM,6BAA6B;AAE/C,KAAI,CAAC,MAAM,QAAQ,aAAa,CAAE,OAAM,IAAI,MAAM,oBAAoB;CAEtE,MAAM,YAAY,aAAa,IAAI,cAAc;AACjD,wBAAuB,UAAU,KAAK,aAAa,SAAS,IAAI,CAAC;AACjE,QAAO;EAAE;EAAS;EAAW;EAAkB;EAAW;;AAG5D,SAAS,cAAc,UAAwC;AAC7D,KAAI,CAAC,SAAS,SAAS,CAAE,OAAM,IAAI,MAAM,mBAAmB;CAE5D,MAAM,MAAM,SAAS;CACrB,MAAM,WAAW,SAAS;CAC1B,MAAM,iBAAiB,SAAS;CAChC,MAAM,eAAe,SAAS;CAC9B,MAAM,aAAa,SAAS;CAC5B,MAAM,MAAM,SAAS;AACrB,KAAI,CAAC,iBAAiB,IAAI,IAAI,IAAI,SAAS,KAAK,CAC9C,OAAM,IAAI,MAAM,uBAAuB;AAEzC,KAAI,aAAa,QAAQ,CAAC,iBAAiB,SAAS,CAClD,OAAM,IAAI,MAAM,4BAA4B;AAE9C,KAAI,OAAO,mBAAmB,UAC5B,OAAM,IAAI,MAAM,kCAAkC;AAEpD,KACE,iBAAiB,KAAA,KACjB,iBAAiB,QACjB,CAAC,iBAAiB,aAAa,CAE/B,OAAM,IAAI,MAAM,wBAAwB;AAE1C,KACE,eAAe,KAAA,KACf,eAAe,QACf,CAAC,kBAAkB,WAAW,CAE9B,OAAM,IAAI,MAAM,sBAAsB;AAExC,KAAI,QAAQ,KAAA,KAAa,QAAQ,QAAQ,CAAC,iBAAiB,IAAI,CAC7D,OAAM,IAAI,MAAM,uBAAuB;AAGzC,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACD;;AAGH,SAAS,SAAS,WAA0D;AAC1E,QAAO,OAAO,cAAc,YAAY,cAAc;;AAGxD,SAAS,iBAAiB,WAAyC;AACjE,QAAO,OAAO,cAAc,YAAY,UAAU,SAAS;;AAG7D,SAAS,kBAAkB,WAAyC;AAClE,QACE,OAAO,cAAc,YACrB,OAAO,UAAU,UAAU,IAC3B,YAAY;;;;;;;;;;;;;;;;;;;;;;ACtLhB,IAAa,aAAb,MAAa,WAAW;CAOtB,aAA0C,KAAA;CAE1C,YACE,WACA,QACA;AAFiB,OAAA,YAAA;AACA,OAAA,SAAA;;;;;;;;;;;CAYnB,aAAa,KAAK,WAAmB,SAAsC;EACzE,MAAM,YAAY,KAAK,WAAW,eAAe;EACjD,MAAM,SAAS,KAAK,WAAW,OAAO;EACtC,MAAM,cAAc,KAAK,WAAW,WAAW;AAM/C,MAAI,WAAW,OAAO;OACL,kBAAkB,YAAY,KAC9B,QACb,QAAO,QAAQ;IAAE,WAAW;IAAM,OAAO;IAAM,CAAC;;EAIpD,MAAM,OAAO,IAAI,WAAW,WAAW,OAAO;AAE9C,MAAI,CAAC,WAAW,OAAO,EAAE;AACvB,aAAU,WAAW,EAAE,WAAW,MAAM,CAAC;AACzC,SAAM,KAAK,IAAI;IAAC;IAAQ;IAAU;IAAM;IAAQ;IAAO,EAAE,EACvD,KAAK,WACN,CAAC;;AAMJ,gBAAc,aAAa,GAAG,QAAQ,KAAK,QAAQ;EAQnD,MAAM,eAAe,KAAK,WAAW,aAAa;AAClD,MAAI,CAAC,WAAW,aAAa,CAC3B,eACE,cACA,0EACD;AAEH,QAAM,+BAA+B,UAAU;AAE/C,SAAO;;;CAIT,MAAM,UAA4B;AAChC,MAAI,KAAK,eAAe,KAAA,EAAW,QAAO,KAAK;AAC/C,MAAI;AACF,SAAM,KAAK,IAAI;IAAC;IAAa;IAAY;IAAO,CAAC;AACjD,QAAK,aAAa;UACZ;AACN,QAAK,aAAa;;AAEpB,SAAO,KAAK;;;;CAKd,MAAM,gBAAkC;AACtC,MAAI,CAAE,MAAM,KAAK,SAAS,CAAG,QAAO;EACpC,MAAM,EAAE,WAAW,MAAM,KAAK,IAAI;GAAC;GAAO;GAAM;GAAe,CAAC;AAChE,SAAO,OAAO,SAAS,OAAO,CAAC,MAAM,KAAK;;;;;;;CAQ5C,MAAM,YAA+B;AACnC,MAAI,CAAE,MAAM,KAAK,SAAS,CAAG,QAAO,EAAE;EAWtC,MAAM,EAAE,WAAW,MAAM,KAAK,IAAI;GAChC;GACA;GACA;GACA;GACA;GACD,CAAC;AACF,SAAO,OACJ,SAAS,OAAO,CAChB,MAAM,KAAK,CACX,QAAQ,SAAS,KAAK,SAAS,EAAE;;;;;;;CAQtC,MAAM,WAAW,MAAsC;AACrD,MAAI,CAAE,MAAM,KAAK,SAAS,CAAG,QAAO;AACpC,MAAI;GACF,MAAM,EAAE,WAAW,MAAM,KAAK,IAAI;IAAC;IAAY;IAAM,QAAQ;IAAO,CAAC;AACrE,UAAO;UACD;AACN,UAAO;;;;;;;;CASX,MAAM,UAAU,SAA2C;EACzD,MAAM,MAAM,OAAO,YAAY,WAAW,OAAO,KAAK,QAAQ,GAAG;EACjE,MAAM,EAAE,WAAW,MAAM,KAAK,IAAI;GAAC;GAAe;GAAM;GAAU,EAAE,EAClE,OAAO,KACR,CAAC;AACF,SAAO,OAAO,SAAS,OAAO,CAAC,MAAM;;;;;;;;;;CAWvC,MAAM,YACJ,OACA,SACiB;EACjB,MAAM,YAAY,YAAY,KAAK,QAAQ,EAAE,gBAAgB,CAAC,GAAG;AAEjE,MAAI;AACF,OAAI,MAAM,SAAS,EAGjB,OAAM,KAAK,IAAI;IAAC;IAAgB;IAAM;IAAe,EAAE;IACrD,OAAO,OAAO,KACZ,MAAM,KAAK,EAAE,MAAM,UAAU,UAAU,IAAI,IAAI,KAAK,IAAI,CAAC,KAAK,GAAG,CAClE;IACD,KAAK,EAAE,gBAAgB,WAAW;IACnC,CAAC;GAGJ,MAAM,WACJ,MAAM,KAAK,IAAI,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,gBAAgB,WAAW,EAAE,CAAC,EACtE,OACC,SAAS,OAAO,CAChB,MAAM;GAET,MAAM,SAAU,MAAM,KAAK,SAAS,IAC/B,MAAM,KAAK,IAAI,CAAC,aAAa,OAAO,CAAC,EAAE,OAAO,SAAS,OAAO,CAAC,MAAM,GACtE;GAEJ,MAAM,aAAa;IAAC;IAAe;IAAS;IAAM;IAAQ;AAC1D,OAAI,OAAQ,YAAW,KAAK,MAAM,OAAO;GAEzC,MAAM,aACJ,MAAM,KAAK,IAAI,YAAY,EACzB,KAAK;IACH,iBAAiB;IACjB,kBAAkB;IAClB,oBAAoB;IACpB,qBAAqB;IACtB,EACF,CAAC,EACF,OACC,SAAS,OAAO,CAChB,MAAM;AAET,SAAM,KAAK,IAAI;IAAC;IAAc;IAAmB;IAAU,CAAC;AAC5D,QAAK,aAAa;AAClB,UAAO;YACC;AAER,OAAI;AACF,WAAO,WAAW,EAAE,OAAO,MAAM,CAAC;AAClC,WAAO,UAAU,UAAU,GAAG,UAAU,SAAS,EAAgB,EAAE;KACjE,WAAW;KACX,OAAO;KACR,CAAC;WACI;;;;;;;;;;;;;;;;;;;;CAuBZ,MAAM,OACJ,MACA,OACA,QACA,OACoD;EACpD,MAAM,MAAM,YAAY,KAAK,QAAQ,EAAE,eAAe,CAAC;EACvD,MAAM,YAAY,KAAK,KAAK,QAAQ;EACpC,MAAM,WAAW,KAAK,KAAK,OAAO;EAClC,MAAM,aAAa,KAAK,KAAK,SAAS;AAEtC,MAAI;AACF,iBAAc,WAAW,MAAM;AAC/B,iBAAc,UAAU,QAAQ,OAAO,MAAM,EAAE,CAAC;AAChD,iBAAc,YAAY,OAAO;AAOjC,OAAI;IACF,MAAM,EAAE,WAAW,MAAM,KAAK,IAAI;KAChC;KACA;KACA,GAAI,UAAU,UACV,CAAC,SAAS,GACV,UAAU,WACR,CAAC,WAAW,GACZ,EAAE;KACR;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACA;KACD,CAAC;AACF,WAAO;KAAE,QAAQ;KAAQ,cAAc;KAAO;YACvC,KAAK;IACZ,MAAM,IAAI;IAeV,MAAM,SACJ,EAAE,kBAAkB,SAChB,EAAE,SACF,EAAE,UAAU,OACV,OAAO,KAAK,EAAE,OAAO,GACrB,OAAO,MAAM,EAAE;IACvB,MAAM,kBACJ,OAAO,EAAE,SAAS,YAAY,EAAE,QAAQ,KAAK,EAAE,QAAQ;AAOzD,QAAI,mBAAmB,MACrB,QAAO;KAAE;KAAQ,cAAc;KAAO;AAExC,QAAI,mBAAmB,OAAO,SAAS,EACrC,QAAO;KAAE;KAAQ,cAAc;KAAM;AAEvC,UAAM;;YAEA;AACR,OAAI;AACF,WAAO,KAAK;KAAE,WAAW;KAAM,OAAO;KAAM,CAAC;WACvC;;;;;;;;;;;CAcZ,MAAM,oBACJ,OACA,SACe;EACf,MAAM,UAAgD,EAAE;AACxD,OAAK,MAAM,EAAE,MAAM,aAAa,MAC9B,SAAQ,KAAK;GAAE;GAAM,KAAK,MAAM,KAAK,UAAU,QAAQ;GAAE,CAAC;AAE5D,QAAM,KAAK,YAAY,SAAS,QAAQ;;CAG1C,MAAc,IACZ,MACA,OAII,EAAE,EACuC;EAG7C,MAAM,QAAQ,MAAM,OADlB,KAAK,OAAO,SAAS,OAAO;GAAC;GAAa,KAAK;GAAQ,GAAG;GAAK,EAC5B;GACnC,KAAK,KAAK,OAAO,KAAK;GACtB,KAAK;IAAE,GAAG,QAAQ;IAAK,GAAG,KAAK;IAAK;GACpC,OAAO;IAAC;IAAQ;IAAQ;IAAO;GAChC,CAAC;AAEF,SAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,SAAmB,EAAE;GAC3B,MAAM,SAAmB,EAAE;AAC3B,SAAM,OAAO,GAAG,SAAS,UAAkB,OAAO,KAAK,MAAM,CAAC;AAC9D,SAAM,OAAO,GAAG,SAAS,UAAkB,OAAO,KAAK,MAAM,CAAC;AAC9D,SAAM,GAAG,SAAS,OAAO;AACzB,SAAM,MAAM,GAAG,SAAS,OAAO;AAC/B,SAAM,GAAG,UAAU,SAAS;IAC1B,MAAM,MAAM,OAAO,OAAO,OAAO;IACjC,MAAM,MAAM,OAAO,OAAO,OAAO;AACjC,QAAI,SAAS,EACX,SAAQ;KAAE,QAAQ;KAAK,QAAQ;KAAK,CAAC;SAChC;KACL,MAAM,oBAAI,IAAI,MACZ,OAAO,KAAK,KAAK,IAAI,CAAC,eAAe,KAAK,IAAI,IAAI,SAAS,OAAO,GACnE;AACD,OAAE,OAAO,QAAQ;AACjB,OAAE,SAAS;AACX,OAAE,SAAS;AACX,YAAO,EAAE;;KAEX;AACF,SAAM,MAAM,IAAI,KAAK,MAAM;IAC3B;;;;;;;;AASN,SAAgB,YAAY,SAA0B;AAIpD,QADa,QAAQ,SAAS,GAAG,KAAK,IAAI,QAAQ,QAAQ,IAAK,CAAC,CACpD,SAAS,EAAE;;;AAIzB,SAAgB,aAAa,MAA6B;AACxD,KAAI;AACF,SAAO,aAAa,KAAK;SACnB;AACN,SAAO;;;;;;;;AASX,SAAS,kBAAkB,aAAoC;AAC7D,KAAI;EACF,MAAM,SAAS,SAAS,aAAa,aAAa,QAAQ,CAAC,MAAM,EAAE,GAAG;AACtE,SAAO,OAAO,SAAS,OAAO,GAAG,SAAS;SACpC;AACN,SAAO;;;;;;;;;;AAWX,eAAe,+BACb,WACe;AASf,KAAI,CARc,MAAM,IAAI,SAAkB,YAAY;EACxD,MAAM,QAAQ,MAAM,OAAO,CAAC,aAAa,wBAAwB,EAAE;GACjE,KAAK;GACL,OAAO;IAAC;IAAU;IAAQ;IAAO;GAClC,CAAC;AACF,QAAM,GAAG,UAAU,SAAS,QAAQ,SAAS,EAAE,CAAC;AAChD,QAAM,GAAG,eAAe,QAAQ,MAAM,CAAC;GACvC,CACc;CAEhB,MAAM,gBAAgB,KAAK,WAAW,aAAa;CACnD,IAAI,WAAW;AACf,KAAI;AACF,aAAW,aAAa,eAAe,QAAQ;SACzC;AACN,aAAW;;CAGb,MAAM,QAAQ,SAAS,MAAM,KAAK,CAAC,KAAK,SAAS,KAAK,MAAM,CAAC;AAG7D,KAAI,MAAM,SAAS,gBAAgB,IAAI,MAAM,SAAS,eAAe,CACnE;CAGF,MAAM,YACJ,SAAS,WAAW,KAAK,SAAS,SAAS,KAAK,GAAG,KAAK;AAC1D,eACE,eACA,GAAG,WAAW,UAAU,kBACxB,QACD;;;;ACpeH,MAAM,YAAY;AAClB,MAAM,kBAAkB;AAExB,SAAS,WAAW,GAA6B;CAC/C,MAAM,SAAS,EAAE,WAAW,WAAW,IAAI,MAAM,MAAM,WAAW,KAAK;AACvE,QAAO,GAAG,EAAE,KAAK,KAAK,EAAE,GAAG,GAAG;;AAGhC,SAAS,aACP,WACA,SACkB;CAClB,MAAM,UAA4B,UAAU,KAAK,OAAO;EACtD,OAAO,WAAW,EAAE;EACpB,OAAO,EAAE;EACV,EAAE;AACH,KAAI,QACF,SAAQ,KAAK;EACX,OAAO,MAAM,IAAI,yBAAyB;EAC1C,OAAO;EACR,CAAC;AAEJ,QAAO;;AAGT,eAAe,gBACb,KACA,MACA,aAIC;CACD,MAAM,OAAO,MAAMK,sBAA6B,KAAK;EACnD,UAAU;EACV;EACA,GAAI,cAAc,EAAE,cAAc,aAAa,GAAG,EAAE;EACrD,CAAC;AAGF,QAAO;EAAE,QAFI,KAAK,sBAAsB,EAAE;EAEnB,SAAS,QADb,KAAK,MAAM,eAAe;EACM;;AAGrD,eAAsB,YACpB,KACA,SAC2B;CAC3B,MAAM,YAAgC,EAAE;CACxC,IAAI,OAAO;CACX,IAAI,UAAU;CACd,IAAI,eAAe;CAGnB,IAAI,cAAc;CAClB,IAAI,gBAAoC,EAAE;AAE1C,QAAO,MAAM;AACX,MAAI,WAAW,UAAU,SAAS,OAAO,WAAW;GAClD,MAAM,SAAS,MAAM,gBAAgB,KAAK,KAAK;AAC/C,aAAU,KAAK,GAAG,OAAO,OAAO;AAChC,aAAU,OAAO;;AAGnB,MAAI,CAAC,UAAU,QAAQ;AACrB,WAAQ,MAAM,mBAAmB;AACjC,WAAQ,KAAK,EAAE;;EAGjB,MAAM,UAAU,aAAa,WAAW,QAAQ;EAEhD,MAAM,EAAE,OAAO,MAAM,QACnB;GACE,MAAM;GACN,MAAM;GACN;GACA,SAAS;GACT;GACA,SAAS,OAAO,OAAe,YAA8B;AAC3D,QAAI,CAAC,OAAO;AACV,mBAAc;AACd,qBAAgB,EAAE;AAClB,YAAO;;AAGT,QAAI,UAAU,aAAa;AACzB,mBAAc;AACd,SAAI;AAEF,uBADe,MAAM,gBAAgB,KAAK,GAAG,MAAM,EAC5B;aACjB;AACN,sBAAgB,EAAE;;;AAItB,WAAO,cAAc,KAAK,OAAO;KAC/B,OAAO,WAAW,EAAE;KACpB,OAAO,EAAE;KACV,EAAE;;GAEN,EACD,EAAE,gBAAgB,QAAQ,KAAK,IAAI,EAAE,CACtC;AAED,MAAI,OAAO,iBAAiB;AAC1B,kBAAe,UAAU;AACzB;AACA;;AAGF,MAAI,CAAC,IAAI;AACP,WAAQ,MAAM,qBAAqB;AACnC,WAAQ,KAAK,EAAE;;EAIjB,MAAM,QACJ,UAAU,MAAM,MAAM,EAAE,OAAO,GAAG,IAClC,cAAc,MAAM,MAAM,EAAE,OAAO,GAAG;AACxC,MAAI,MAAO,QAAO;AAIlB,UADa,MAAMC,oBAA2B,KAAK,GAAG,EAC1C;;;AAIhB,eAAsB,UACpB,KACA,YAC2B;CAE3B,MAAM,QAAQ,OAAO,WAAW;AAChC,KAAI,OAAO,UAAU,MAAM,IAAI,QAAQ,EACrC,KAAI;EACF,MAAM,OAAO,MAAMA,oBAA2B,KAAK,MAAM;AACzD,MAAI,KAAK,kBAAmB,QAAO,KAAK;SAClC;CAMV,IAAI,OAAO;CACX,IAAI,UAAU;AACd,QAAO,SAAS;EACd,MAAM,SAAS,MAAM,gBAAgB,KAAK,MAAM,WAAW;EAC3D,MAAM,QAAQ,OAAO,OAAO,MACzB,MAAM,EAAE,KAAK,aAAa,KAAK,WAAW,aAAa,CACzD;AACD,MAAI,MAAO,QAAO;AAClB,YAAU,OAAO;AACjB;;AAGF,SAAQ,MAAM,mCAAmC,aAAa;AAC9D,SAAQ,KAAK,EAAE;;;;ACnJjB,MAAM,iBAAiB;;;;;AAMvB,SAAgB,cAAc,UAA0C;CACtE,IAAI,MAAM,QAAQ,YAAY,QAAQ,KAAK,CAAC;AAG5C,QAAO,MAAM;EACX,MAAM,YAAY,KAAK,KAAK,eAAe;AAC3C,MAAI,WAAW,UAAU,CACvB,KAAI;GACF,MAAM,MAAM,aAAa,WAAW,QAAQ;GAC5C,MAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,UAAO;IAAE,MAAM;IAAK;IAAQ;UACtB;AACN,UAAO;;EAGX,MAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,WAAW,IAAK;AACpB,QAAM;;AAGR,QAAO;;;;;;;;;;;;AAaT,SAAgB,wBACd,WACe;CACf,MAAM,MAAM,QAAQ,QAAQ,KAAK,CAAC;CAClC,MAAM,WAAW,KAAK,UAAU,MAAM,QAAQ;CAC9C,MAAM,MAAM,SAAS,UAAU,IAAI;AAGnC,KAAI,IAAI,WAAW,KAAK,IAAI,QAAQ,IAAK,QAAO;CAIhD,MAAM,eAAe,IAAI,MAAM,IAAI,CAAC;AACpC,KAAI,CAAC,aAAc,QAAO;AAE1B,QAAO,KAAK,UAAU,aAAa;;;;;ACnCrC,SAAgB,yBAAyB,eAAiC;AACxE,QACE,CAAC,iBAAiB,QAAQ,IAAI,2CAA2C;;;;;;;;;;AAY7E,eAAsB,uBACpB,KACA,eACA,MACgE;AAChE,KAAI,kBAAkB,KAAA,EACpB,KAAI;AAMF,SAAO;GAAE,QALI,MAAMC,oCACjB,KACA,eACA,EAAE,mBAAmB,EAAE,MAAM,EAAE,CAChC,EACoB;GAAmB,iBAAiB;GAAM;UACxD,OAAO;AACd,MACE,CAAC,WAAW,MAAM,IACjB,MAAM,WAAW,OAAO,MAAM,WAAW,IAE1C,OAAM;AAGR,UAAQ,KACN,wHACD;;AAOL,QAAO;EAAE,QAHI,MAAMC,uBAA8B,KAAK,EACpD,mBAAmB;GAAE;GAAM,QAAQ;GAAe,EACnD,CAAC,EACmB;EAAmB,iBAAiB;EAAO;;AAGlE,eAAe,eACb,KACA,YACA,YACA,eACgE;AAChE,KAAI,YAAY;EACd,MAAM,QAAQ,MAAM,UAAU,KAAK,WAAW;AAE9C,oBAAkB,MAAM,GAAG;AAC3B,SAAO;GAAE;GAAO,iBAAiB;GAAO;;CAK1C,MAAM,SAAS,YAAY,WAAW;AAItC,KAAI,UAAU,OAAO,kBAAkB,eAAe;AACpD,MAAI;GAEF,MAAM,YADO,MAAMC,oBAA2B,KAAK,OAAO,GAAG,EACvC;AACtB,OAAI,YAAY,SAAS,WAAW,eAAe;AACjD,YAAQ,IAAI,6BAA6B,SAAS,KAAK;AAEvD,gBAAY,YAAY;KACtB,GAAG;KACH,IAAI,SAAS;KACb,MAAM,SAAS;KACf,GAAI,kBAAkB,KAAA,IAAY,EAAE,GAAG,EAAE,eAAe;KACzD,CAAC;AACF,WAAO;KAAE,OAAO;KAAU,iBAAiB;KAAO;;UAE9C;AAIR,gBAAc,WAAW;;CAI3B,MAAM,EAAE,aAAa,MAAM,OAAO;CAQlC,MAAM,WAAW,MAAM,uBAAuB,KAAK,eALjD,gBAFW,UAAU,CAAC,MAAM,IAAI,CAAC,MAAM,MAElB,GAAG,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,GAAG,MAChE,GACA,GACD,CAEoE;CACvE,MAAM,EAAE,UAAU;AAMlB,aAAY,YALkB;EAC5B,IAAI,MAAM;EACV,MAAM,MAAM;EACZ,GAAI,kBAAkB,KAAA,IAAY,EAAE,GAAG,EAAE,eAAe;EACzD,CACgC;AACjC,SAAQ,IAAI,sBAAsB,MAAM,KAAK,KAAK,MAAM,GAAG,GAAG;AAC9D,QAAO;;AAGT,SAAgB,mBAA4B;AAC1C,QAAO,IAAI,QAAQ,MAAM,CACtB,YAAY,6CAA6C,CACzD,OAAO,iBAAiB,qBAAqB,YAAY,CACzD,OAAO,iBAAiB,qBAAqB,OAAO,CACpD,OACC,4BACA,6CACD,CACA,OAAO,eAAe,mCAAmC,CACzD,OAAO,wBAAwB,gCAAgC,YAAY,CAC3E,OAAO,cAAc,6CAA6C,CAClE,OAAO,iBAAiB,wBAAwB,IAAI,CACpD,OACC,OAAO,SAQD;AACJ,gBAAc;EAGd,IAAI,WAAW,KAAK;AACpB,MAAI,aAAa,KAAK;GACpB,MAAM,YAAY,eAAe;AACjC,OAAI,UACF,YAAW,wBAAwB,UAAU,IAAI;;EAIrD,MAAM,YAAY,IAAI,UAAU,SAAS;AACzC,MAAI,CAAC,UAAU,SAAS,EAAE;AACxB,WAAQ,MAAM,IAAI,SAAS,yCAAyC;AACpE,WAAQ,KAAK,EAAE;;EAGjB,MAAM,OAAO,OAAO,KAAK,KAAK;AAC9B,MAAI,CAAC,OAAO,UAAU,KAAK,IAAI,OAAO,KAAK,OAAO,OAAO;AACvD,WAAQ,MACN,kBAAkB,KAAK,KAAK,4CAC7B;AACD,WAAQ,KAAK,EAAE;;AAOjB,MAAI;AACF,SAAM,mBAAmB,KAAK,MAAM,KAAK;WAClC,GAAG;AACV,OAAI,aAAa,eACf,SAAQ,MAAM,EAAE,QAAQ;OAExB,SAAQ,MAAM,sCAAsC,IAAI;AAE1D,WAAQ,KAAK,EAAE;;EAGjB,MAAM,aAAa,KAAK,eAAe,QAAQ,QAAQ;EACvD,MAAM,MAAM,iBAAiB;EAC7B,MAAM,SAAS,gBAAgB,UAAU,KAAK;EAG9C,IAAI;AACJ,MAAI,QAAQ,QACV,WAAU,OAAO;OACZ;AAIL,cAHmB,MAAM,IAAI,IAC3B,+BACD,EACoB,MAAM,SAAS,aAAa;AACjD,OAAI,CAAC,SAAS;AACZ,YAAQ,MACN,wEACD;AACD,YAAQ,KAAK,EAAE;;;EAQnB,MAAM,aAAa,YAAY,SAAS,UAAU,KAAK;EACvD,MAAM,YAAY,KAAK,QACnB,MAAM,eAAe,KAAK,YAAY,KAAK,MAAM,GACjD,MAAM,eAAe,KAAK,YAAY,KAAA,GAAW,QAAQ,QAAQ;EACrE,MAAM,EAAE,UAAU;EAClB,MAAM,gBAAgB,IAAI,mBAAmB,UAAU,KAAK;EAC5D,MAAM,kBAAkB,yBAAyB,QAAQ,KAAK,MAAM,CAAC;EACrE,IAAI,qBAAqB,kBACrB,sBACE,UAAU,MACV,MAAM,IACN,cAAc,aAAa,CAC5B,GACD;AACJ,MACE,CAAC,sBACD,mBACA,UAAU,mBACV,QAAQ,WACR,OAAO,QAEP,KAAI;AAKF,wBAAqB,MAAM,+BACzB,WALmB,MAAM,WAAW,KACpC,UAAU,MACV,OAAO,QACR,EAIC,MAAM,IACN,OAAO,QACR;AACD,OAAI,mBACF,wBAAuB,UAAU,MAAM,mBAAmB;UAEtD;AACN,wBAAqB;;EAGzB,MAAM,YAAY,kCAAkC,MAAM,GAAG;EAE7D,IAAI;EAEJ,MAAM,gBAAgB;AACpB,WAAQ;AACR,WAAQ,KAAK,EAAE;;AAEjB,UAAQ,GAAG,UAAU,QAAQ;AAC7B,UAAQ,GAAG,WAAW,QAAQ;AAE9B,SAAO,MAAM,eACX,KACA;GACE,IAAI,MAAM;GACV,MAAM,MAAM;GACZ;GACA;GACD,EACD,WACA;GACE,MAAM,KAAK;GACX;GACA;GACA,UAAU,CAAC,KAAK;GAChB,GAAI,qBAAqB,EAAE,aAAa,oBAAoB,GAAG,EAAE;GACjE,GAAI,kBACA;IACE,gBAAgB,UACd,uBAAuB,UAAU,MAAM,MAAM;IAC/C,gCACE,wBAAwB,UAAU,KAAK;IAC1C,GACD,EAAE;GACP,GACA,YAAY;AACX,WAAQ,IAAI,mBAAmB,UAAU;AACzC,WAAQ,IAAI,iBAAiB,YAAY;AACzC,WAAQ,IAAI,mCAAmC;AAE/C,OAAI,KAAK,SACP,QAAO,QAAQ,MAAM,MAAM,EAAE,QAAQ,GAAG,QAAQ,OAAO,CAAC;IAG7D;AAGD,QAAM,IAAI,cAAc,GAAG;GAE9B;;;;;;;;;;;;;ACrSL,eAAsB,kBACpB,WACA,QACkB;CAClB,MAAM,UAAuB,EAAE;CAC/B,MAAM,UAAoB,EAAE;CAE5B,MAAM,aAAa,UAAU,OAAO;CACpC,MAAM,6BAAa,IAAI,KAAwB;CAC/C,MAAM,gBAAgB,IAAI,mBAAmB,UAAU,KAAK;CAC5D,IAAI;AACJ,KAAI;AACF,cAAY,MAAM,OAAO,WAAW;UAC7B,OAAO;AACd,QAAM,IAAI,MAAM,yCAAyC,EAAE,OAAO,OAAO,CAAC;;AAG5E,wBAAuB,CACrB,GAAG,WAAW,KAAK,SAAS,KAAK,aAAa,EAC9C,GAAG,UACJ,CAAC;CACF,MAAM,cAAc,IAAI,IAAI,UAAU;AAEtC,MAAK,MAAM,QAAQ,YAAY;AAC7B,MAAI,CAAC,KAAK,OAAQ;AAClB,aAAW,IAAI,KAAK,cAAc,KAAK;EAEvC,MAAM,WAAW,MAAM,OAAO,WAAW,KAAK,aAAa;AAC3D,MAAI,CAAC,YAAY,YAAY,IAAI,KAAK,aAAa,CACjD,OAAM,IAAI,MACR,0CAA0C,KAAK,eAChD;EAEH,MAAM,WAAW,KAAK,SAAS,OAAO,KAAK,KAAK,MAAM,CAAC,GAAG,KAAK,YAAY;AAC3E,MAAI,YAAY,SAAS,OAAO,SAAS,CAAE;AAE3C,UAAQ,KAAK,KAAK;;AAGpB,MAAK,MAAM,OAAO,WAAW;AAC3B,MAAI,WAAW,IAAI,IAAI,CAAE;AACzB,MAAI,UAAU,OAAO,OAAO,IAAI,CAAE;AAGlC,MAAI,cAAc,IAAI,IAAI,CAAE;AAC5B,MAAI,gBAAgB,IAAI,CAAE;AAC1B,UAAQ,KAAK,IAAI;;AAGnB,QAAO;EAAE;EAAS;EAAS;;;;;;;;;AAU7B,SAAgB,wBAAwB,OAA8B;CACpE,MAAM,UAAoB,EAAE;AAC5B,MAAK,MAAM,QAAQ,OAAO;AACxB,MAAI,CAAC,KAAK,OAAQ;EAClB,MAAM,MAAM,aAAa,KAAK,aAAa;AAC3C,MAAI,CAAC,IAAK;AACV,MAAI,uBAAuB,IAAI,CAAE,SAAQ,KAAK,KAAK,aAAa;;AAElE,QAAO;;;;;;;;;;;;AAaT,SAAgB,wBAAwB,OAAyB;AAC/D,QAAO,wDAAwD,MAAM,KAAK,IAAI;;AAGhF,MAAM,iBAAiB,OAAO,KAAK,UAAU;AAC7C,MAAM,eAAe,OAAO,KAAK,UAAU;AAC3C,MAAM,eAAe,OAAO,KAAK,UAAU;;;;;;;AAQ3C,SAAS,uBAAuB,KAAsB;AACpD,QACE,IAAI,SAAS,eAAe,IAC5B,IAAI,SAAS,aAAa,IAC1B,IAAI,SAAS,aAAa;;;;;;;AAS9B,eAAsB,kBACpB,WACA,QACA,SACe;CACf,MAAM,UAAgD,EAAE;CACxD,MAAM,gBAAgB,IAAI,mBAAmB,UAAU,KAAK;CAC5D,MAAM,4BAAY,IAAI,KAAa;AACnC,MAAK,MAAM,QAAQ,UAAU,OAAO,EAAE;AACpC,MAAI,CAAC,KAAK,OAAQ;AAClB,YAAU,IAAI,KAAK,aAAa;EAIhC,MAAM,MAAM,KAAK,SAAS,OAAO,KAAK,KAAK,MAAM,CAAC,GAAG,KAAK,YAAY;AACtE,UAAQ,KAAK;GAAE,MAAM,KAAK;GAAc,KAAK,MAAM,OAAO,UAAU,IAAI;GAAE,CAAC;;CAE7E,IAAI;AACJ,MAAK,MAAM,OAAO,cAAc,MAAM,EAAE;AACtC,MAAI,UAAU,IAAI,IAAI,CAAE;AACxB,8BAA4B,MAAM,OAAO,UACvC,8BACD;AACD,UAAQ,KAAK;GACX,MAAM;GACN,KAAK;GACN,CAAC;;AAIJ,KAAI,QAAQ,SAAS,KAAM,MAAM,OAAO,SAAS,CAC/C,OAAM,OAAO,YAAY,SAAS,QAAQ;;;;;;;;AAU9C,eAAsB,yBACpB,QACA,WACA,QACe;AACf,QAAO,oCAAoC;AAC3C,OAAM,kBACJ,WACA,QACA,2BAAU,IAAI,MAAM,EAAC,aAAa,GACnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AClIH,eAAsB,uBAAuB,OAMb;CAC9B,MAAM,EAAE,QAAQ,WAAW,QAAQ,aAAa,YAAY;CAC5D,MAAM,SAA6B;EACjC,QAAQ;EACR,UAAU,EAAE;EACZ,YAAY,EAAE;EACd,QAAQ,EAAE;EACX;AAED,KAAI,MAAM,OAAO,SAAS,CAAE,QAAO;CAEnC,MAAM,UAAgD,EAAE;AAExD,MAAK,MAAM,YAAY,QAAQ;EAC7B,MAAM,OAAO,UAAU,KAAK,SAAS,IAAI;AAGzC,MAAI,CAAC,KAAK,aAAa,WAAW,UAAU,OAAO,IAAI,EAAE;AACvD,UAAO,OAAO,KAAK,YAAY,SAAS,IAAI,2BAA2B;AACvE;;AAGF,MAAI,CAAC,KAAK,QAAQ;AAChB,UAAO,WAAW,KAAK,SAAS,IAAI;AACpC;;EAGF,IAAI;AACJ,MAAI,SAAS,YAAY,KAAK,UAAU,KAAK,SAAS,SAEpD,WAAU,KAAK,SAAS,OAAO,KAAK,KAAK,MAAM,CAAC,GAAG,KAAK,YAAY;MAEpE,KAAI;AACF,aAAU,MAAM,kBAAkB,UAAU,YAAY;WACjD,GAAG;AACV,UAAO,OAAO,KACZ,YAAY,SAAS,IAAI,IAAI,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,GACxE;AACD;;AAGJ,MAAI,WAAW,KAGb;AAGF,UAAQ,KAAK;GAAE,MAAM,SAAS;GAAK,KAAK,MAAM,OAAO,UAAU,QAAQ;GAAE,CAAC;AAC1E,SAAO,SAAS,KAAK,SAAS,IAAI;;AAGpC,KAAI,QAAQ,SAAS,GAAG;AACtB,QAAM,OAAO,YAAY,SAAS,QAAQ;AAC1C,SAAO,SAAS;;AAGlB,QAAO;;AAGT,eAAe,kBACb,UACA,aACwB;AACxB,KAAI,SAAS,kBAAkB,kBAAkB,SAAS,IACxD,QAAO,YAAY,SAAS,IAAI;AAElC,KAAI,SAAS,WAAW,KAAM,QAAO;CACrC,MAAM,OACJ,OAAO,SAAS,YAAY,WACxB,SAAS,UACT,KAAK,UAAU,SAAS,QAAQ;AACtC,QAAO,OAAO,KAAK,KAAK;;;;;;;;;;;;;;;;;;;;;;;AC5G1B,eAAsB,iCAAiC,OAKrC;CAChB,MAAM,EAAE,QAAQ,WAAW,cAAc,YAAY;AAErD,KAAI,MAAM,OAAO,SAAS,CAAE;CAE5B,MAAM,SAAS,sBAAsB,aAAa;AAClD,KAAI,CAAC,OAAQ;AACb,KAAI,OAAO,YAAY,QAAS;AAChC,KAAI,CAAC,OAAO,aAAa,OAAO,KAAK,OAAO,UAAU,CAAC,WAAW,EAAG;CAErE,MAAM,OAAiD,EAAE;AACzD,MAAK,MAAM,QAAQ,UAAU,OAAO,EAAE;AACpC,MAAI,CAAC,KAAK,OAAQ;EAClB,MAAM,SAAS,OAAO,UAAU,KAAK;AACrC,MAAI,CAAC,OAAQ;AACb,MAAI,KAAK,UAAU,KAAK,OAAQ;EAEhC,MAAM,UAAU,KAAK,SAAS,OAAO,KAAK,KAAK,MAAM,CAAC,GAAG,KAAK,YAAY;AAC1E,OAAK,KAAK;GAAE,MAAM,KAAK;GAAc;GAAS,CAAC;;AAGjD,KAAI,KAAK,WAAW,EAAG;AAEvB,OAAM,OAAO,oBACX,MACA,oDAAmC,IAAI,MAAM,EAAC,aAAa,GAC5D;;;;;ACpBH,MAAM,eAAyC;CAC7C,MAAM;CACN,MAAM;CACN,wBAAwB;CACxB,wBAAwB;CACzB;;;;;;;;;;AAcD,SAAgB,kBACd,MACA,OACA,uBAAa,IAAI,MAAM,EACvB,UAAkC,EAAE,EAC5B;AAER,QAAO,qBADQ,iBAAiB,QAAQ,IAAI,aAAa,OACrB,OAAO,KAAK;;;;;;;AAQlD,SAAgB,YACd,KACkB;AAClB,QAAO,mBAAmB,IAAI;;;;;;;;AAShC,eAAsB,eACpB,KACA,YAAY,KACe;AAG3B,QAAO,0BAA0B,iBAAiB,KAAK,UAAU,CAAC;;AAGpE,eAAe,iBACb,KACA,WAC2B;AAC3B,KAAI;AAYF,SAAO,YAXK,MAAM,QAAQ,KAAK,CAC7B,IAAI,IAA6B,UAAU,EAC3C,IAAI,SAAgB,GAAG,WAAW;AAClB,oBACN,uBAAO,IAAI,MAAM,mBAAmB,UAAU,IAAI,CAAC,EACzD,UACD,CAEK,SAAS;IACf,CACH,CAAC,CACqB;UAChB,KAAK;EACZ,MAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC/D,UAAQ,KACN,mEAAmE,OAAO,GAC3E;AACD,SAAO;;;;;AC5EX,SAAgB,oBAA6B;AAC3C,QAAO,IAAI,QAAQ,OAAO,CACvB,YAAY,2CAA2C,CACvD,OAAO,4BAA4B,8BAA8B,CACjE,OAAO,kBAAkB,6CAA6C,CACtE,OACC,eACA,+DACD,CACA,OAAO,iBAAiB,kCAAkC,CAC1D,OACC,qBACA,gDACD,CACA,OACC,mBACA,oLAGD,CACA,OAAO,iBAAiB,wBAAwB,IAAI,CACpD,OACC,OAAO,SAQD;AACJ,gBAAc;EAEd,IAAI,WAAW,KAAK;AACpB,MAAI,aAAa,KAAK;GACpB,MAAM,YAAY,eAAe;AACjC,OAAI,UACF,YAAW,wBAAwB,UAAU,IAAI;;EAIrD,MAAM,YAAY,IAAI,UAAU,SAAS;AACzC,MAAI,CAAC,UAAU,SAAS,EAAE;AACxB,WAAQ,MAAM,IAAI,SAAS,yCAAyC;AACpE,WAAQ,KAAK,EAAE;;EAGjB,MAAM,MAAM,iBAAiB;EAC7B,MAAM,SAAS,gBAAgB,UAAU,KAAK;EAC9C,IAAI;AAEJ,MAAI,KAAK,aAAa;GACpB,MAAM,EAAE,SAAS,MAAM,QACrB;IACE,MAAM;IACN,MAAM;IACN,SAAS;IACV,EACD,EAAE,gBAAgB,QAAQ,KAAK,IAAI,EAAE,CACtC;AACD,OAAI,CAAC,MAAM;AACT,YAAQ,MAAM,0BAA0B;AACxC,YAAQ,KAAK,EAAE;;AAKjB,YAHa,MAAMC,uBAA8B,KAAK,EACpD,mBAAmB;IAAE;IAAM,QAAQ;IAAS,EAC7C,CAAC,EACW;AACb,WAAQ,IACN,8BAA8B,MAAM,KAAK,KAAK,MAAM,GAAG,GACxD;aACQ,KAAK,MACd,SAAQ,MAAM,UAAU,KAAK,KAAK,MAAM;WAC/B,QAAQ;AACjB,WAAQ,IACN,yCAAyC,MAAM,KAAK,OAAO,UAAU,CAAC,KAAK,OAAO,QAAQ,GAC3F;AAED,YADa,MAAMC,oBAA2B,KAAK,OAAO,QAAQ,EACrD;QAEb,SAAQ,MAAM,YAAY,KAAK,4BAA4B;EAG7D,MAAM,SAAS,MAAM,WAAW,KAAK,UAAU,MAAM,MAAM,GAAG;AAQ9D,QAAM,iCAAiC;GACrC;GACA;GACA,cAAc,UAAU;GACxB,SAAS,MAAM;GAChB,CAAC;EAEF,MAAM,aAAa,UAAU,OAAO,CAAC,QAAQ,MAAM,EAAE,OAAO;EAI5D,IAAI,eAAiD;EACrD,MAAM,iBACH,iBAAiB,eAAe,IAAI;EAQvC,MAAM,aAAa,wBAAwB,WAAW;AACtD,MAAI,WAAW,SAAS,GAAG;AACzB,WAAQ,KAAK;AACb,WAAQ,IACN,MAAM,IACJ,KAAK,WAAW,OAAO,qDACxB,CACF;AACD,QAAK,MAAM,OAAO,WAAY,SAAQ,IAAI,KAAK,MAAM;AACrD,WAAQ,KAAK;AACb,WAAQ,IACN,qCAAqC,MAAM,KAAK,UAAU,CAAC,KAAK,MAAM,KAAK,UAAU,CAAC,YACvF;AACD,WAAQ,IAAI,iBAAiB,MAAM,KAAK,mBAAmB,CAAC,GAAG;AAC/D,WAAQ,KAAK;AACb,OAAI,KAAK,aACP,SAAQ,MAAM,wBAAwB,WAAW,CAAC;AAEpD,WAAQ,KAAK,EAAE;;EAGjB,MAAM,SAAS,IAAI,OAAO,KAAK,MAAM,IAAI,UAAU;EAUnD,MAAM,qBAAqB,QAAQ,YAAY,MAAM;EACrD,IAAI,UACF,KAAK,SAAS,CAAC,qBAAqB,OAAQ,QAAQ,WAAW;EAOjE,MAAM,cAAc,WAAW,QAAS,MAAM,OAAO,SAAS;AAQ9D,MAAI,CAAC,KAAK,SAAS,CAAC,KAAK,eAAe,CAAC,aAAa;AACpD,OAAI,CAAC,KAAK,cAAc;AACtB,YAAQ,OAAO;AACf,YAAQ,MACN,MAAM,IACJ,gCAAgC,MAAM,KAAK,MAAM,MAAM,GAAG,IAC3D,CACF;AACD,YAAQ,OAAO;AACf,YAAQ,MACN,SAAS,MAAM,KAAK,uBAAuB,MAAM,KAAK,CAAC,+CACxD;AACD,YAAQ,MACN,wEACD;AACD,YAAQ,OAAO;AACf,YAAQ,MACN,oBAAoB,MAAM,KAAK,kBAAkB,CAAC,uCACnD;AACD,YAAQ,MACN,yFACD;AACD,YAAQ,OAAO;AACf,YAAQ,MACN,iFACD;AACD,YAAQ,MACN,qCAAqC,MAAM,KAAK,UAAU,CAAC,GAC5D;AACD,YAAQ,OAAO;AACf,YAAQ,KAAK,EAAE;;GAQjB,MAAM,kBAAkB,IACtB,0DAA0D,MAAM,KAAK,KAAK,MAAM,GAAG,IACpF,CAAC,OAAO;AACT,OAAI;IAEF,MAAM,aAAa,MAAM,uBAAuB;KAC9C;KACA;KACA,QAJgB,MAAM,OAAO,aAAa;KAK1C,cAAc,QAAQ,OAAO,oBAAoB,IAAI;KACrD,SAAS,kBACP,wBACA,MAAM,UAAU,CACjB;KACF,CAAC;AACF,cAAU,OAAO,WAAW,IAAI;IAChC,MAAM,QAAQ,CAAC,YAAY,WAAW,SAAS,OAAO,UAAU;AAChE,QAAI,WAAW,WAAW,SAAS,EACjC,OAAM,KACJ,QAAQ,WAAW,WAAW,OAAO,gCACtC;AAEH,oBAAgB,QAAQ,uBAAuB,MAAM,KAAK,KAAK,CAAC,GAAG;AACnE,SAAK,MAAM,OAAO,WAAW,OAC3B,SAAQ,KAAK,KAAK,MAAM,OAAO,OAAO,CAAC,GAAG,MAAM;YAE3C,GAAG;AACV,oBAAgB,KACd,gDAAgD,YAAY,EAAE,GAC/D;AACD,YAAQ,KAAK,EAAE;;;EAMnB,MAAM,EAAE,SAAS,YAAY,MAAM,kBAAkB,WAAW,OAAO;EACvE,MAAM,oBAAoB,IAAI,mBAAmB,UAAU,KAAK,CAAC,MAAM,CACpE;AAEH,MACE,QAAQ,WAAW,KACnB,QAAQ,WAAW,KACnB,sBAAsB,GACtB;AACA,WAAQ,IAAI,yDAAyD;AACrE,SAAM,eAAe;AACrB;;EAGF,MAAM,UAAU,IAAI,cAAc,MAAM,KAAK,KAAK,MAAM,GAAG,IAAI,CAAC,OAAO;AAIvE,MAAI,CAAC,KAAK,OAAO;GACf,MAAM,mBAA6B,EAAE;AACrC,QAAK,MAAM,QAAQ,SAAS;AAC1B,QAAI,CAAC,KAAK,SAAU;AACpB,SAAK,MAAM,cAAc,KAAK,gBAAgB,CAC5C,KAAI,WAAW,aAAa,QAC1B,kBAAiB,KACf,GAAG,KAAK,aAAa,IAAI,WAAW,UACrC;AAGL,SAAK,MAAM,cAAc,8BACvB,KAAK,MAAM,CACZ,CACC,kBAAiB,KACf,GAAG,KAAK,aAAa,IAAI,WAAW,UACrC;;AAGL,OAAI,iBAAiB,SAAS,GAAG;AAC/B,YAAQ,KACN,6BAA6B,iBAAiB,OAAO,kCACtD;AACD,SAAK,MAAM,SAAS,iBAAkB,SAAQ,MAAM,KAAK,QAAQ;AACjE,YAAQ,KAAK,EAAE;;;AAGnB,MAAI;AACF,SAAM,OAAO,cAAc,QAAQ;AACnC,aAAU,OAAO,WAAW,IAAI;WACzB,GAAG;AACV,OAAI,aAAa,mBAAmB;AAClC,oBAAgB,QAAQ;AACxB,YAAQ,KAAK,EAAE;;AAEjB,SAAM;;EAGR,IAAI,SAAS;AACb,MAAI;AACF,YAAS,MAAM,OAAO,kBAAkB,EAAE,SAAS,MAAM,CAAC;AAI1D,aAAU,OAAO,WAAW,IAAI;WACzB,OAAO;AACd,WAAQ,KACN,2CAA2C,YAAY,MAAM,GAC9D;AACD,WAAQ,KAAK,EAAE;;AAGjB,MAAI,QAAQ,WAAW,KAAK,QAAQ,WAAW,KAAK,WAAW,GAAG;AAChE,OAAI;AACF,UAAM,yBAAyB,QAAQ,WAAW,OAAO;YAClD,OAAO;AACd,YAAQ,KACN,+CAA+C,YAAY,MAAM,GAClE;AACD,YAAQ,KAAK,EAAE;;AAEjB,WAAQ,QAAQ,oDAAoD;AACpE,SAAM,eAAe;AACrB;;EAGF,IAAI,WAAW;EACf,IAAI,eAAe;EACnB,MAAM,SAAmB,EAAE;EAC3B,IAAI,WAAW;EACf,MAAM,QAAQ,QAAQ,UAAU,KAAK,WAAW,IAAI,QAAQ;AAE5D,OAAK,MAAM,QAAQ,SAAS;AAC1B,OAAI;AACF,UAAM,OAAO,WAAW,MAAM,QAAQ;AACtC,cAAU,OAAO,WAAW,IAAI;AAChC;YACO,GAAG;AACV,QAAI,aAAa,mBAAmB;AAClC,aAAQ,MAAM;AACd,qBAAgB,KAAK,CAAC;AACtB,aAAQ,KAAK,EAAE;;AAEjB,WAAO,KAAK,UAAU,KAAK,aAAa,IAAI,YAAY,EAAE,GAAG;;AAE/D,WAAQ,OAAO,WAAW,EAAE,SAAS,GAAG,MAAM;;AAGhD,MAAI,CAAC,KAAK,SACR,MAAK,MAAM,OAAO,SAAS;AACzB,OAAI;AACF,UAAM,OAAO,iBAAiB,KAAK,QAAQ;AAC3C,cAAU,OAAO,WAAW,IAAI;AAChC;YACO,GAAG;AACV,QAAI,aAAa,mBAAmB;AAClC,aAAQ,MAAM;AACd,qBAAgB,KAAK,CAAC;AACtB,aAAQ,KAAK,EAAE;;AAEjB,WAAO,KAAK,UAAU,IAAI,IAAI,YAAY,EAAE,GAAG;;AAEjD,WAAQ,OAAO,WAAW,EAAE,SAAS,GAAG,MAAM;;AAIlD,MAAI,OAAO,WAAW,EACpB,KAAI;AAGF,UAAO,oCAAoC;WACpC,OAAO;AACd,UAAO,KAAK,0BAA0B,YAAY,MAAM,GAAG;;AAc/D,MAAI,WAAW,KAAK,eAAe,KAAK,SAAS,EAC/C,OAAM,OAAO,aAAa;AAG5B,MAAI,OAAO,QAAQ;AACjB,WAAQ,KAAK,eAAe,OAAO,OAAO,YAAY;AACtD,QAAK,MAAM,OAAO,OAAQ,SAAQ,MAAM,KAAK,MAAM;AASnD,WAAQ,WAAW;QAEnB,SAAQ,QACN,UAAU,SAAS,kBAAkB,OAAO,+BACzC,eAAe,IACZ,aAAa,aAAa,oBAC1B,KACP;AASH,MAAI,OAAO,WAAW,GAAG;GAKvB,MAAM,gBAA+B,CACnC,GAAG,QAAQ,KAAK,UAAU;IACxB,QAAQ;IACR,MAAM,KAAK;IACZ,EAAE,EACH,GAAI,KAAK,WACL,EAAE,GACF,QAAQ,KAAK,SAAS;IACpB,QAAQ;IACR,MAAM;IACP,EAAE,CACR;AACD,SAAM,kBACJ,WACA,QACA,kBACE,QACA,MAAM,UAAU,kBAChB,IAAI,MAAM,EACV,cACD,CACF;;AAGH,QAAM,eAAe;;;;;;;;;EAUrB,eAAe,gBAA+B;AAC5C,OAAI,QAAQ;AACV,qBAAiB,UAAU,MAAM;KAC/B,SAAS,MAAM;KACf,WAAW,MAAM;KACjB,SAAS,OAAO;KAChB,SAAS,WAAW,KAAA;KACpB,kBAAkB,IAAI,mBACpB,UAAU,KACX,CAAC,YAAY,EAAE,gBAAgB,MAAM,CAAC;KACxC,CAAC;AACF;;AAEF,OAAI,CAAC,KAAK,aAAc;AACxB,OAAI;IAIF,MAAM,aAHM,MAAM,IAAI,IAEnB,+BAA+B,EACZ,MAAM,SAAS;AACrC,QAAI,CAAC,UAAW;AAChB,qBAAiB,UAAU,MAAM;KAC/B,SAAS,MAAM;KACf,WAAW,MAAM;KACjB,SAAS;KACT,SAAS,WAAW,KAAA;KACpB,kBAAkB,IAAI,mBACpB,UAAU,KACX,CAAC,YAAY,EAAE,gBAAgB,MAAM,CAAC;KACxC,CAAC;WACI;;AAMV,MAAI,KAAK,SAAS;GAChB,MAAM,aAAa,IAAI,oBAAoB,CAAC,OAAO;AACnD,OAAI;AACF,UAAMC,wBAA+B,KAAK,MAAM,GAAG;AACnD,eAAW,QAAQ,mBAAmB;YAC/B,GAAG;AACV,eAAW,KAAK,mBAAmB,IAAI;AAGvC,YAAQ,WAAW;;;GAI1B;;AAGL,SAAS,gBAAgB,SAAuC;AAC9D,SAAQ,KAAK,yDAAyD;AACtE,SAAQ,KAAK;AACb,SAAQ,IACN,KAAK,MAAM,KAAK,+BAA+B,CAAC,+BACjD;AACD,SAAQ,IACN,2BAA2B,MAAM,KAAK,2BAA2B,CAAC,uBACnE;AACD,SAAQ,KAAK;;;;ACvhBf,SAAS,gBAAgB,WAA4B;AACnD,QAAO,YAAY,WAAW,EAAE,eAAe,MAAM,CAAC,CAAC,MAAM,UAAU;AACrE,MAAI,MAAM,KAAK,WAAW,IAAI,IAAI,MAAM,SAAS,eAC/C,QAAO;AACT,SACE,MAAM,gBAAgB,IACrB,MAAM,aAAa,IAAI,gBAAgB,KAAK,WAAW,MAAM,KAAK,CAAC;GAEtE;;;;AAKJ,eAAsB,yBACpB,MACA,SACkB;AAClB,KAAI;AACF,MAAI,gBAAgB,KAAK,CAAE,QAAO;EAClC,MAAM,SAAS,gBAAgB,KAAK;AACpC,MACE,CAAC,UACD,OAAO,YAAY,WACnB,CAAC,WAAW,KAAK,MAAM,gBAAgB,QAAQ,OAAO,CAAC,CAEvD,QAAO;AAKT,MAJiB,aACf,KAAK,MAAM,gBAAgB,WAAW,EACtC,OACD,CAAC,MAAM,KACS,OAAO,QAAQ,CAAE,QAAO;EACzC,MAAM,eAAe,KAAK,MAAM,qBAAqB;AACrD,MAAI,WAAW,aAAa,EAAE;GAC5B,MAAM,MAAe,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC;AACnE,OACE,CAAC,OACD,OAAO,QAAQ,YACf,EAAE,aAAa,QACf,IAAI,YAAY,KAChB,EAAE,YAAY,QACd,CAAC,IAAI,UACL,OAAO,IAAI,WAAW,YACtB,MAAM,QAAQ,IAAI,OAAO,CAEzB,QAAO;GACT,MAAM,WAAW,IAAI,mBAAmB,KAAK;AAC7C,OAAI,SAAS,SAAS,CAAC,MAAM,GAAG,UAAU,KAAK,QAAQ,CAAE,QAAO;AAChE,OACE,CAAC,OAAO,oBACR,OAAO,qBAAqB,SAAS,aAAa,CAElD,QAAO;aAET,OAAO,oBACP,OAAO,qBAAqB,IAAI,mBAAmB,KAAK,CAAC,aAAa,CAEtE,QAAO;EACT,MAAM,SAAS,MAAM,WAAW,KAAK,MAAM,QAAQ;AACnD,MAAI,CAAE,MAAM,OAAO,eAAe,CAAG,QAAO;EAC5C,MAAM,OAAO,MAAM,kBAAkB,IAAI,UAAU,KAAK,EAAE,OAAO;AACjE,SAAO,KAAK,QAAQ,WAAW,KAAK,KAAK,QAAQ,WAAW;SACtD;AACN,SAAO;;;;;;ACjDX,MAAM,oBAAoB;;;;;;;;AAS1B,MAAM,gBACJ;;;;;;;;;;;;;;;AA0EF,eAAsB,UACpB,OAC0B;CAC1B,MAAM,EAAE,WAAW,QAAQ,QAAQ,aAAa,eAAe;CAC/D,MAAM,WAAW,MAAM;CACvB,MAAM,SAA0B;EAC9B,SAAS;EACT,QAAQ;EACR,WAAW,EAAE;EACb,cAAc,EAAE;EAChB,SAAS;EACT,SAAS;EACT,QAAQ,EAAE;EACX;CAMD,MAAM,gBAMD,EAAE;CAQP,MAAM,gCAAgB,IAAI,KAAa;CACvC,MAAM,YAAY,KAAa,MAAiB,YAA6B;AAC3E,MAAI;AACF,OAAI,MAAM,gBACR,OAAM,gBAAgB,OAAO,CAAC,IAAI,QAAQ,KAAK,MAAM,QAAQ,CAAC;OAC3D,MAAK,MAAM,QAAQ;AACxB,UAAO;WACA,GAAG;AACV,OAAI,aAAa,2BAA4B,OAAM;AACnD,iBAAc,IAAI,IAAI;AACtB,UAAO,OAAO,KAAK,aAAa,IAAI,IAAI,OAAO,EAAE,CAAC,IAAI,gBAAgB;AACtE,UAAO;;;;;;;;;;CAUX,MAAM,SACJ,KACA,MACA,SACA,QACA,mBAAmB,UACV;AACT,MAAI,MAAM,SAAS;AACjB,iBAAc,KAAK;IAAE;IAAK;IAAM;IAAS;IAAQ;IAAkB,CAAC;AACpE;;AAEF,MAAI,SAAS,KAAK,MAAM,QAAQ,CAAE,SAAQ;;CAG5C,MAAM,gCAAgB,IAAI,KAAqB;CAC/C,MAAM,6BAAa,IAAI,KAAa;CACpC,IAAI,OAAO;AACX,MAAK,MAAM,YAAY,QAAQ;AAC7B,aAAW,IAAI,SAAS,IAAI;AAC5B,MAAI,MAAM,gBAAgB,IAAI,SAAS,IAAI,EAAE;AAC3C,gBAAa,EAAE,MAAM,OAAO,OAAO;AACnC;;AAEF,MAAI;GACF,MAAM,MAAM,MAAM,YAAY,UAAU,YAAY;AACpD,OAAI,IAAK,eAAc,IAAI,SAAS,KAAK,IAAI;WACtC,GAAG;AACV,UAAO,OAAO,KAAK,YAAY,SAAS,IAAI,IAAI,OAAO,EAAE,GAAG;;AAE9D,eAAa,EAAE,MAAM,OAAO,OAAO;;AAGrC,OAAM,iBAAiB,iBAAiB;AACxC,MAAK,MAAM,CAAC,KAAK,cAAc,eAAe;EAC5C,MAAM,OAAO,UAAU,KAAK,IAAI;AAEhC,MAAI,CAAC,KAAK,aAAa,WAAW,UAAU,OAAO,IAAI,EAAE;AACvD,UAAO,OAAO,KAAK,aAAa,IAAI,2BAA2B;AAC/D,iBAAc,IAAI,IAAI;AACtB;;AAGF,MAAI,MAAM,OAAO;AAGf,SAAM,KAAK,MAAM,iBAAiB,OAAO,UAAU;AACnD;;EAGF,MAAM,WAAW,aAAa,KAAK,aAAa;EAChD,MAAM,UAAU,MAAM,OAAO,WAAW,IAAI;AAE5C,MAAI,YAAY,MAAM;AACpB,SAAM,KAAK,MAAM,iBAAiB,OAAO,UAAU;AACnD;;AAGF,MAAI,SAAS,OAAO,UAAU,EAAE;AAC9B,UAAO;AACP;;AAGF,MAAI,WAAW,SAAS,OAAO,QAAQ,EAAE;AACvC,SAAM,KAAK,MAAM,iBAAiB,OAAO,UAAU;AACnD;;AAGF,MAAI,WAAW,UAAU,OAAO,QAAQ,EAAE;AAMxC,OACE,MAAM,YAAY,YAClB,CAAC,YAAY,SAAS,IACtB,4BAA4B,SAAS,EACrC;AACA,UACE,KACA,MACA,iBACM,OAAO,aAAa,KAAK,GAAG,IAAI,6BAA6B,EACnE,kBACD;AACD;;AAGF,UAAO;AACP;;AAQF,MAJE,YAAY,SAAS,IACrB,YAAY,UAAU,KACrB,UAAU,YAAY,QAAQ,GAAG,QAEtB;AAEZ,OAAI,MAAM,YAAY,SAAS;AAE7B,WAAO,aAAa,KAAK,GAAG,IAAI,wBAAwB;AACxD;;AAKF,SACE,KACA,MACA,iBACM;AACJ,QAAI,MAAM,YAAY,SACpB,QAAO,aAAa,KAAK,GAAG,IAAI,yBAAyB;QAEzD,QAAO,UAAU,KAAK,GAAG,IAAI,yBAAyB;MAG1D,MAAM,YAAY,SACnB;AACD;;EAMF,MAAM,EAAE,QAAQ,iBAAiB,MAAM,OAAO,OAC5C,SACA,UACA,UACD;AACD,MAAI,gBAAgB,MAAM,SAAS;AAOjC,SACE,KACA,OARc,MAAM,OAAO,OAC3B,SACA,UACA,WACA,MAAM,QACP,EAIS,cACF,OAAO,aAAa,KAAK,IAAI,EACnC,kBACD;AACD;;AAEF,QAAM,KAAK,MAAM,cAAc;AAC7B,OAAI,aAAc,QAAO,UAAU,KAAK,IAAI;OACvC,QAAO;IACZ;;AAMJ,KAAI,MAAM,SAAS;AACjB,MAAI,cAAc,MAAM,MAAM,EAAE,iBAAiB,CAC/C,OAAM,kBACJ,WACA,QACA,kBAAkB,wBAAwB,MAAM,SAAS,KAAK,CAC/D;AAEH,QAAM,iBAAiB,iBAAiB;AACxC,OAAK,MAAM,EAAE,KAAK,MAAM,SAAS,YAAY,cAC3C,KAAI,SAAS,KAAK,MAAM,QAAQ,CAAE,SAAQ;;AAI9C,KAAI,YAAa,MAAM,OAAO,SAAS,CACrC,MAAK,MAAM,QAAQ,UAAU,OAAO,EAAE;AACpC,MAAI,WAAW,IAAI,KAAK,aAAa,CAAE;AAIvC,MAAI,gBAAgB,KAAK,aAAa,CAAE;EAExC,MAAM,UAAU,MAAM,OAAO,WAAW,KAAK,aAAa;AAC1D,MAAI,CAAC,QAAS;EACd,MAAM,WAAW,aAAa,KAAK,aAAa;AAChD,MAAI,CAAC,SAAU;AACf,MAAI,CAAC,SAAS,OAAO,QAAQ,CAAE;AAE/B,MAAI;AACF,OAAI,MAAM,gBACR,OAAM,gBAAgB,OAAO,CAAC,KAAK,aAAa,QAC9C,WAAW,KAAK,aAAa,CAC9B;OACE,YAAW,KAAK,aAAa;AAClC,UAAO;WACA,OAAO;AACd,OAAI,iBAAiB,2BAA4B,OAAM;;;CAsB7D,MAAM,gBAAsD,EAAE;AAC9D,MAAK,MAAM,CAAC,KAAK,QAAQ,eAAe;AACtC,MAAI,cAAc,IAAI,IAAI,CAAE;AAC5B,gBAAc,KAAK;GAAE,MAAM;GAAK,KAAK,MAAM,OAAO,UAAU,IAAI;GAAE,CAAC;;AAErE,MAAK,MAAM,OAAO,YAAY;AAC5B,MAAI,MAAM,gBAAgB,IAAI,IAAI,CAAE;AAEpC,MAAI,cAAc,IAAI,IAAI,IAAI,CAAC,cAAc,IAAI,IAAI,CAAE;EACvD,MAAM,WAAW,MAAM,OAAO,WAAW,IAAI;AAC7C,MAAI,YAAY,KAAM;AACtB,gBAAc,KAAK;GAAE,MAAM;GAAK,KAAK,MAAM,OAAO,UAAU,SAAS;GAAE,CAAC;;CAE1E,MAAM,gBAAgB,IAAI,mBAAmB,UAAU,KAAK;CAC5D,IAAI;AACJ,MAAK,MAAM,OAAO,MAAM,kBAAkB,EAAE,EAAE;AAI5C,MAAI,CAAC,WAAW,IAAI,IAAI,IAAI,CAAC,cAAc,IAAI,IAAI,CAAE;AACrD,8BAA4B,MAAM,OAAO,UACvC,8BACD;AACD,gBAAc,KAAK;GACjB,MAAM;GACN,KAAK;GACN,CAAC;;AASJ,KACE,cAAc,SAAS,KACtB,WAAW,SAAS,KAAM,MAAM,OAAO,SAAS,EACjD;AACA,QAAM,iBAAiB,iBAAiB;AACxC,QAAM,OAAO,YACX,eACA,kBAAkB,QAAQ,MAAM,SAAS,KAAK,CAC/C;;AAGH,QAAO;;AAGT,SAAS,4BAA4B,SAA0B;CAC7D,MAAM,OAAO,QAAQ,SAAS,OAAO;AACrC,QACE,sBAAsB,KAAK,KAAK,IAChC,gBAAgB,KAAK,KAAK,IAC1B,uBAAuB,KAAK,KAAK;;AAIrC,eAAe,YACb,UACA,aACwB;AACxB,KAAI,SAAS,kBAAkB,kBAAkB,SAAS,IACxD,QAAO,YAAY,SAAS,IAAI;AAElC,KAAI,SAAS,WAAW,KAAM,QAAO;CACrC,MAAM,OACJ,OAAO,SAAS,YAAY,WACxB,SAAS,UACT,KAAK,UAAU,SAAS,QAAQ;AACtC,QAAO,OAAO,KAAK,KAAK;;AAG1B,SAAS,OAAO,GAAoB;AAClC,QAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE;;;;ACpanD,eAAe,sBACb,KACiB;CAEjB,MAAM,aADM,MAAM,IAAI,IAAe,+BAA+B,EAC9C,MAAM,SAAS;AACrC,KAAI,CAAC,WAAW;AACd,UAAQ,MACN,wEACD;AACD,UAAQ,KAAK,EAAE;;AAEjB,QAAO;;AAGT,SAAgB,oBAA6B;AAC3C,QAAO,IAAI,QAAQ,OAAO,CACvB,YAAY,8CAA8C,CAC1D,OAAO,4BAA4B,2BAA2B,CAC9D,OAAO,kBAAkB,8CAA8C,CACvE,OAAO,iBAAiB,uBAAuB,CAC/C,OAAO,aAAa,2BAA2B,CAC/C,OACC,mBACA,gEACD,CACA,OACC,eACA,0DACD,CACA,OACC,oBACA,uKAGD,CACA,OACC,OAAO,SAQD;EACJ,IAAI;AACJ,MAAI;AACF,iBAAc;AAEd,OACE,KAAK,YAAY,KAAA,KACjB,KAAK,YAAY,WACjB,KAAK,YAAY,UACjB;AACA,YAAQ,MACN,4BAA4B,KAAK,QAAQ,8BAC1C;AACD,YAAQ,KAAK,EAAE;;GAEjB,MAAM,cAAc,KAAK;GAEzB,MAAM,MAAM,iBAAiB;GAC7B,MAAM,YAAY,eAAe;GAEjC,MAAM,QAAQ,KAAK,QACf,MAAM,UAAU,KAAK,KAAK,MAAM,GAChC,MAAM,YAAY,KAAK,yBAAyB;GAEpD,MAAM,YAAY,MAAM,sBAAsB,IAAI;GAClD,IAAI;AACJ,OAAI,KAAK,KACP,QAAO,KAAK;YACH,UACT,QACE,wBAAwB,UAAU,IAClC,KAAK,UAAU,MAAM,SAAS,UAAU;OAE1C,QAAO;GAGT,MAAM,eAAe,QAAQ,KAAK;GAClC,MAAM,iBAAiB,gBAAgB,aAAa;GACpD,MAAM,kBAAkB,KAAK,cACzB,IAAI,oBAAoB,aAAa,GACrC,KAAA;AAEJ,WAAQ,KAAK;AACb,WAAQ,IAAI,cAAc,MAAM,KAAK,MAAM,KAAK,CAAC,KAAK,MAAM,GAAG,GAAG;AAClE,WAAQ,IAAI,cAAc,MAAM,KAAK,UAAU,GAAG;AAClD,WAAQ,IAAI,cAAc,MAAM,KAAK,aAAa,GAAG;AACrD,WAAQ,KAAK;AAEb,OAAI,CAAC,KAAK,KAAK;IACb,MAAM,EAAE,cAAc,MAAM,QAC1B;KACE,MAAM;KACN,MAAM;KACN,SAAS;KACT,SAAS;KACV,EACD,EAAE,gBAAgB,QAAQ,KAAK,IAAI,EAAE,CACtC;AACD,QAAI,CAAC,WAAW;AACd,aAAQ,IAAI,WAAW;AACvB,aAAQ,KAAK,EAAE;;;AAInB,OACE,KAAK,eACL,CAAE,MAAM,yBAAyB,cAAc,MAAM,GAAG,EACxD;AACA,YAAQ,IAAI,sDAAsD;AAClE,YAAQ,KAAK,EAAE;;AAEjB,oBAAiB,iBAAiB;GAClC,MAAM,YAAY,IAAI,UAAU,KAAK;GACrC,MAAM,SAAS,MAAM,WAAW,KAAK,cAAc,MAAM,GAAG;AAC5D,SAAM,iCAAiC;IACrC;IACA;IACA;IACA,SAAS,MAAM;IAChB,CAAC;GACF,MAAM,SAAS,IAAI,OAAO,KAAK,MAAM,IAAI,UAAU;GAInD,MAAM,eAAe,eAAe,IAAI;AAExC,aAAU,IAAI,WAAW,MAAM,KAAK,KAAK,MAAM,GAAG,IAAI,CAAC,OAAO;GAC9D,MAAM,YAAY,MAAM,OACrB,aAAa,CACb,OAAO,UAAmB;AACzB,QAAI,EAAE,iBAAiB,oBAAqB,OAAM;AAClD,QAAI,QAAS,SAAQ,KAAK,MAAM,QAAQ;AACxC,YAAQ,KAAK,EAAE;KACf;AAEJ,OACE,KAAK,eACL,CAAE,MAAM,yBAAyB,cAAc,MAAM,GAAG,EACxD;AACA,YAAQ,MAAM;AACd,YAAQ,IAAI,kDAAkD;AAC9D,YAAQ,KAAK,EAAE;;GAEjB,MAAM,qBAAqB,MAAM,OAAO,wBACtC,WACA;IACE,QAAQ,CAAC,KAAK;IACd;IACD,CACF;GAED,MAAM,SAAS,MAAM,UAAU;IAC7B;IACA;IACA;IACA,QAAQ;IACR,cAAc,QAAQ,OAAO,oBAAoB,IAAI;IACrD,QAAQ,CAAC,KAAK;IACd,OAAO,KAAK,SAAS;IACrB,gBAAgB,mBAAmB;IACnC,SAAS;IACT,OAAO,MAAM;IACb,aAAa,MAAM,UAAU;AAC3B,SAAI,QAAS,SAAQ,OAAO,eAAe,KAAK,GAAG,MAAM;;IAE5D,CAAC;AACF,UAAO,OAAO,KAAK,GAAG,mBAAmB,OAAO;GAEhD,MAAM,QAAkB,EAAE;AAC1B,OAAI,OAAO,UAAU,EAAG,OAAM,KAAK,SAAS,OAAO,QAAQ,UAAU;AACrE,OAAI,OAAO,SAAS,EAClB,OAAM,KAAK,UAAU,OAAO,OAAO,kBAAkB;AACvD,OAAI,mBAAmB,SAAS,EAC9B,OAAM,KACJ,QAAQ,mBAAmB,OAAO,yBACnC;AAEH,OAAI,OAAO,aAAa,SAAS,EAC/B,OAAM,KACJ,iBAAiB,OAAO,aAAa,OAAO,qBAAqB,YAAY,GAC9E;AACH,OAAI,OAAO,UAAU,EACnB,OAAM,KAAK,WAAW,OAAO,QAAQ,gBAAgB;AACvD,OAAI,OAAO,UAAU,EACnB,OAAM,KAAK,GAAG,OAAO,QAAQ,kBAAkB;AAEjD,OAAI,OAAO,OAAO,QAAQ;AACxB,YAAQ,KACN,eAAe,OAAO,OAAO,OAAO,aAAa,MAAM,KAAK,KAAK,CAAC,GACnE;AACD,SAAK,MAAM,KAAK,OAAO,OAAQ,SAAQ,MAAM,KAAK,IAAI;AActD,YAAQ,WAAW;cACV,OAAO,UAAU,SAAS,GAAG;AACtC,YAAQ,KACN,GAAG,OAAO,UAAU,OAAO,iDAAiD,MAAM,KAAK,KAAK,CAAC,GAC9F;AACD,YAAQ,KAAK;AACb,SAAK,MAAM,KAAK,OAAO,UACrB,SAAQ,IAAI,KAAK,MAAM,OAAO,WAAW,CAAC,GAAG,IAAI;AAEnD,YAAQ,KAAK;AACb,YAAQ,IACN,2CAA2C,MAAM,KAAK,UAAU,CAAC,KAAK,MAAM,KAAK,UAAU,CAAC,WAC7F;AACD,YAAQ,IACN,cAAc,MAAM,KAAK,mBAAmB,CAAC,oCAC9C;AACD,YAAQ,KAAK;SAEb,SAAQ,QAAQ,MAAM,KAAK,KAAK,IAAI,sBAAsB;GAM5D,MAAM,YAAY,OAAO,WAAW;AACpC,oBAAiB,iBAAiB;GAClC,MAAM,qBACJ,iBAAiB,cAAc;IAC7B,SAAS,MAAM;IACf,WAAW,MAAM;IACjB,SAAS;IACT,SAAS,aAAa,gBAAgB;IACtC,kBAAkB,IAAI,mBACpB,aACD,CAAC,YAAY,EACZ,gBAAgB,MACjB,CAAC;IACH,CAAC;AAEJ,OAAI,gBACF,iBAAgB,OAAO,CAAC,oBAAoB,EAAE,aAAa;OACxD,eAAc;AACnB,OAAI,OAAO,UAAU,SAAS,EAAG,SAAQ,KAAK,EAAE;WACzC,OAAO;AACd,OAAI,EAAE,iBAAiB,4BAA6B,OAAM;AAC1D,YAAS,MAAM;AACf,WAAQ,IAAI,MAAM,QAAQ;AAC1B,WAAQ,WAAW;;GAGxB;;;;ACtQL,SAAS,cAAc,cAAqC;CAC1D,MAAM,QAAQ,aAAa,MAAM,QAAQ;AACzC,KAAI,MAAM,OAAO,cAAc,MAAM,UAAU,EAC7C,QAAO,MAAM,GAAI,QAAQ,aAAa,GAAG;AAE3C,QAAO;;AAGT,SAAgB,oBAA6B;AAC3C,QAAO,IAAI,QAAQ,OAAO,CACvB,YAAY,uDAAuD,CACnE,OAAO,iBAAiB,wBAAwB,IAAI,CACpD,OAAO,UAAU,iCAAiC,CAClD,OAAO,OAAO,SAA2C;EAGxD,IAAI,WAAW,KAAK;AACpB,MAAI,aAAa,KAAK;GACpB,MAAM,YAAY,eAAe;AACjC,OAAI,UACF,YAAW,wBAAwB,UAAU,IAAI;;EAIrD,MAAM,YAAY,IAAI,UAAU,SAAS;AACzC,MAAI,CAAC,UAAU,SAAS,EAAE;GACxB,MAAM,UAAU,IAAI,SAAS;AAC7B,OAAI,KAAK,KACP,SAAQ,IAAI,KAAK,UAAU;IAAE,IAAI;IAAO,OAAO;IAAS,CAAC,CAAC;OAE1D,SAAQ,MAAM,QAAQ;AAExB,WAAQ,KAAK,EAAE;;EAMjB,MAAM,cAHQ,UAAU,OAAO,CAI5B,QAAQ,MAAM,EAAE,SAAS,CACzB,KAAK,OAAO;GAAE,MAAM;GAAG,SAAS,EAAE,MAAM;GAAE,EAAE;EAE/C,MAAM,yBAAS,IAAI,KAA2B;EAC9C,MAAM,UAAU,MAAc,eAAiC;GAC7D,MAAM,WAAW,OAAO,IAAI,KAAK;AACjC,OAAI,SAAU,UAAS,KAAK,WAAW;OAClC,QAAO,IAAI,MAAM,CAAC,WAAW,CAAC;;AAMrC,OAAK,MAAM,EAAE,MAAM,aAAa,aAAa;GAC3C,MAAM,mBAAqC,KAAK,aAC5C,WACA;AACJ,QAAK,MAAM,cAAc,mBAAmB,SAAS,EACnD,kBACD,CAAC,CACA,QAAO,KAAK,cAAc,WAAW;AAEvC,QAAK,MAAM,cAAc,8BAA8B,QAAQ,CAC7D,QAAO,KAAK,cAAc,WAAW;;EAOzC,MAAM,uCAAuB,IAAI,KAAa;AAC9C,OAAK,MAAM,EAAE,UAAU,aAAa;GAClC,MAAM,OAAO,cAAc,KAAK,aAAa;AAC7C,OAAI,KAAM,sBAAqB,IAAI,KAAK;;EAE1C,MAAM,YAA6B,YAChC,QAAQ,EAAE,WAAW,cAAc,KAAK,aAAa,KAAK,KAAK,CAC/D,KAAK,EAAE,MAAM,eAAe;GAAE,MAAM,KAAK;GAAc;GAAS,EAAE;AACrE,OAAK,MAAM,WAAW,6BACpB,WACA,qBACD,CACC,QAAO,QAAQ,cAAc,QAAQ,WAAW;EAGlD,MAAM,UAA6B,CAAC,GAAG,OAAO,SAAS,CAAC,CACrD,KAAK,CAAC,MAAM,kBAAkB;GAAE;GAAM;GAAa,EAAE,CACrD,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,KAAK,CAAC;EAE/C,IAAI,SAAS;EACb,IAAI,WAAW;AACf,OAAK,MAAM,EAAE,iBAAiB,QAC5B,MAAK,MAAM,KAAK,YACd,KAAI,EAAE,aAAa,QAAS;MACvB;EAOT,MAAM,wBAAwB,QAAQ,MAAM,EAAE,kBAC5C,YAAY,MACT,MACC,EAAE,QAAQ,SAAS,aACnB,EAAE,OAAO,UAAU,UACnB,EAAE,OAAO,gBAAgB,KAAA,EAC5B,CACF;AAED,MAAI,KAAK,KACP,SAAQ,IACN,KAAK,UAAU;GACb,IAAI,WAAW;GACf;GACA;GACA,cAAc,YAAY;GAC1B,GAAI,wBACA,EAAE,mBAAmB,qBAAqB,GAC1C,EAAE;GACN,OAAO;GACR,CAAC,CACH;MAED,WAAU,SAAS,QAAQ,UAAU,YAAY,OAAO;AAG1D,UAAQ,KAAK,SAAS,IAAI,IAAI,EAAE;GAChC;;AAGN,SAAS,OAAO,OAAe,MAAsB;AACnD,QAAO,GAAG,MAAM,GAAG,OAAO,UAAU,IAAI,KAAK;;AAG/C,SAAS,UACP,SACA,QACA,UACA,cACM;AACN,MAAK,MAAM,EAAE,MAAM,iBAAiB,SAAS;AAC3C,UAAQ,IAAI,MAAM,KAAK,KAAK,CAAC;AAC7B,OAAK,MAAM,KAAK,aAAa;GAC3B,MAAM,QACJ,EAAE,aAAa,UACX,MAAM,IAAI,QAAQ,OAAO,EAAE,CAAC,GAC5B,MAAM,OAAO,UAAU,OAAO,EAAE,CAAC;GAGvC,MAAM,UAAU,EAAE,QAAQ,MAAM,KAAK,CAAC;AACtC,WAAQ,IAAI,KAAK,MAAM,GAAG,UAAU;;;CAIxC,MAAM,SAAS,IAAI,OAAO,cAAc,OAAO,CAAC;AAChD,KAAI,SAAS,EACX,SAAQ,IACN,KAAK,MAAM,IAAI,KAAK,OAAO,QAAQ,QAAQ,CAAC,IAAI,OAAO,UAAU,UAAU,GAAG,CAAC,GAAG,SACnF;UACQ,WAAW,EACpB,SAAQ,IACN,KAAK,MAAM,OAAO,KAAK,OAAO,UAAU,UAAU,GAAG,CAAC,GAAG,SAC1D;KAED,SAAQ,IAAI,GAAG,MAAM,MAAM,sBAAsB,CAAC,GAAG,SAAS;;;;ACrLlE,MAAM,oBAAoB;AAE1B,MAAM,eAAe;AAErB,SAAgB,oBAA6B;AAC3C,QAAO,IAAI,QAAQ,OAAO,CACvB,YAAY,mDAAmD,CAC/D,SAAS,UAAU,mCAAmC,CACtD,OAAO,yBAAyB,yBAAyB,kBAAkB,CAC3E,OAAO,OAAO,MAA0B,SAA+B;AACtE,MAAI,CAAC,MAAM;AAST,WARY,MAAM,QAChB;IACE,MAAM;IACN,MAAM;IACN,SAAS;IACV,EACD,EAAE,gBAAgB,QAAQ,KAAK,IAAI,EAAE,CACtC,EACU;AACX,OAAI,CAAC,MAAM;AACT,YAAQ,MAAM,oBAAoB;AAClC,YAAQ,KAAK,EAAE;;;AAInB,MAAI,CAAC,aAAa,KAAK,KAAK,EAAE;AAC5B,WAAQ,MACN,wBAAwB,KAAK,+DAC9B;AACD,WAAQ,KAAK,EAAE;;AAGjB,UAAQ,IAAI,sBAAsB,KAAK,SAAS,QAAQ,KAAK,GAAG;AAChE,eAAa,OAAO;GAAC;GAAS,KAAK;GAAU;GAAK,EAAE,EAAE,OAAO,WAAW,CAAC;AAEzE,OAAK,MAAM,OAAO,CAAC,QAAQ,UAAU,EAAE;GACrC,MAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,OAAI,WAAW,KAAK,CAAE,QAAO,MAAM;IAAE,WAAW;IAAM,OAAO;IAAM,CAAC;;AAGtE,UAAQ,IAAI,4BAA4B,OAAO;AAC/C,UAAQ,IAAI,qBAAqB,KAAK,sBAAsB;GAC5D;;;;AC3CN,SAAS,aACP,OACA,SACkB;AAClB,KAAI,CAAC,MAAO,QAAO;CACnB,MAAM,QAAQ,MAAM,aAAa;AACjC,QAAO,QAAQ,QAAQ,MAAM,EAAE,MAAM,aAAa,CAAC,SAAS,MAAM,CAAC;;AAcrE,MAAM,qBAA6C;CACjD,SAAS;CACT,cAAc;CACd,cAAc;CACd,SAAS;CACT,cAAc;CACd,oBAAoB;CACpB,qBAAqB;CACtB;AAED,MAAM,gBAAgB;CACpB;EAAE,OAAO;EAAQ,MAAM;EAAS;CAChC;EAAE,OAAO;EAAQ,MAAM;EAAc;CACrC;EAAE,OAAO;EAAkB,MAAM;EAAc;CAC/C;EAAE,OAAO;EAAQ,MAAM;EAAS;CAChC;EAAE,OAAO;EAAQ,MAAM;EAAc;CACrC;EAAE,OAAO;EAAoB,MAAM;EAAoB;CACvD;EAAE,OAAO;EAAqB,MAAM;EAAqB;CAC1D;AAED,MAAM,kBAAkB;CACtB;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACD;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACD;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACD;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACD;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACD;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACD;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACD;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACF;AAED,eAAe,sBACb,KACA,SACA,eAC0B;CAC1B,MAAM,SAAS,IAAI,gBAAgB;EACjC,sBAAsB,OAAO,QAAQ;EACrC,gBAAgB;EAChB,WAAW;EACZ,CAAC;AAIF,SAHa,MAAM,IAAI,IACrB,oCAAoC,SACrC,EACW,aAAa,EAAE;;AAG7B,eAAe,eACb,KACA,SACA,eACA,UACwB;CACxB,MAAM,YAAY,MAAM,sBAAsB,KAAK,SAAS,cAAc;AAC1E,KAAI,UAAU,UAAU,EAAG,QAAO;CAMlC,MAAM,EAAE,eAAe,MAAM,QAC3B;EACE,MAAM;EACN,MAAM;EACN,SAAS;EACT,SAToB,UAAU,KAAK,OAAO;GAC5C,OAAO,GAAG,EAAE,OAAO,EAAE,UAAU,eAAe;GAC9C,OAAO,EAAE;GACV,EAAE;EAOC,UAAU,OAAe,YACvB,QAAQ,QAAQ,aAAa,OAAO,QAAQ,CAAC;EAChD,EACD,EAAE,UAAU,CACb;AAED,QAAO,cAAc;;AAGvB,SAAgB,wBAAiC;AAC/C,QAAO,IAAI,QAAQ,WAAW,CAC3B,YAAY,8DAA8D,CAC1E,OAAO,iBAAiB,mBAAmB,YAAY,CACvD,OAAO,iBAAiB,mBAAmB,OAAO,CAClD,OAAO,oBAAoB,0CAA0C,CACrE,OAAO,OAAO,SAAyD;AACtE,gBAAc;EAEd,MAAM,UAAU,KAAK,QAAQ,OAAO,KAAK,MAAM,GAAG,mBAAmB;AAErE,MAAI,CAAC,SAAS;AACZ,WAAQ,MACN,0EACD;AACD,WAAQ,KAAK,EAAE;;EAGjB,MAAM,UAAU,UAAU,KAAK,KAAK,GAAG,KAAK;EAa5C,MAAM,UAAoB,CACxB,GAAG,cAAc,KAAK,OAAO;GAAE,OAAO,EAAE;GAAO,OAAO,EAAE;GAAM,EAAE,EAChE,GAAG,gBAAgB,KAAK,OAAO;GAC7B,OAAO,GAAG,EAAE,MAAM;GAClB,OAAO;IACL,cAAc,EAAE;IAChB,UAAU,EAAE;IACZ,UAAU,EAAE;IACZ,OAAO,EAAE;IACV;GACF,EAAE,CACJ;EAED,MAAM,iBAAiB,QAAQ,KAAK,IAAI;EAExC,MAAM,EAAE,SAAS,MAAM,QACrB;GACE,MAAM;GACN,MAAM;GACN,SAAS;GACT;GACA,UAAU,OAAe,YACvB,QAAQ,QAAQ,aAAa,OAAO,QAAQ,CAAC;GAChD,EACD,EAAE,UAAU,CACb;AAED,MAAI,CAAC,KAAM;EAEX,MAAM,MAAM,iBAAiB;EAC7B,IAAI;EACJ,IAAI;AAEJ,MAAI,OAAO,SAAS,UAAU;AAC5B,UAAO;AACP,mBAAgB,mBAAmB;SAC9B;AACL,mBAAgB,KAAK;GAMrB,MAAM,aALO,MAAMC,uCACjB,KACA,SACA;IAAE,WAAW,KAAK;IAAc,UAAU;IAAI,CAC/C,EACsB,wBAAwB,EAAE;AAEjD,OAAI,CAAC,UAAU,QAAQ;AACrB,YAAQ,IAAI,MAAM,KAAK,MAAM,uCAAuC;AACpE,WAAO,KAAK;UACP;IACL,MAAM,kBAAkB,UAAU,KAAK,OAAO;KAC5C,OAAO,EAAE,SAAS,EAAE,QAAQ;KAC5B,OAAO,EAAE;KACV,EAAE;IACH,MAAM,EAAE,SAAS,MAAM,QACrB;KACE,MAAM;KACN,MAAM;KACN,SAAS,YAAY,KAAK,MAAM,aAAa;KAC7C,SAAS;KACT,UAAU,OAAe,YACvB,QAAQ,QAAQ,aAAa,OAAO,QAAQ,CAAC;KAChD,EACD,EAAE,UAAU,CACb;AACD,WAAO,KAAK,SAAS,QAAQ,MAAM,KAAe;;;EAItD,IAAI,gBAAgB;AACpB,MAAI,eAAe;GACjB,MAAM,aAAa,MAAM,eACvB,KACA,SACA,eACA,SACD;AACD,OAAI,WACF,iBAAgB,sBAAsB;;EAI1C,MAAM,MAAM,GAAG,UAAU,OAAO;AAChC,UAAQ,IAAI,oBAAoB,IAAI,IAAI;EACxC,MAAM,QAAQ,MAAM,OAAO,SAAS;AACpC,QAAM,KAAK,IAAI;GACf;;;;ACrPN,SAAgB,eAAe,WAA6B;AAC1D,KAAI,CAAC,WAAW,UAAU,CAAE,QAAO,EAAE;AACrC,QAAO,YAAY,WAAW,EAAE,eAAe,MAAM,CAAC,CACnD,QAAQ,UAAU,MAAM,aAAa,CAAC,CACtC,KAAK,UAAU,MAAM,KAAK,CAC1B,QAAQ,SAAS,WAAW,KAAK,WAAW,MAAM,WAAW,CAAC,CAAC,CAC/D,MAAM;;AA+BX,eAAsB,cACpB,SAC8B;CAC9B,MAAM,EAAE,WAAW,YAAY,OAAO,kBAAkB,eACtD;CAEF,MAAM,YAAsB,EAAE;CAC9B,MAAM,UAAoB,EAAE;AAE5B,WAAU,YAAY,EAAE,WAAW,MAAM,CAAC;AAE1C,MAAK,MAAM,QAAQ,eAAe,UAAU,EAAE;EAC5C,MAAM,OAAO,KAAK,WAAW,KAAK;EAClC,MAAM,KAAK,KAAK,YAAY,KAAK;AAGjC,MAFe,WAAW,GAAG,IAEf,CAAC,SAAS,CAAE,MAAM,iBAAiB,KAAK,EAAG;AACvD,WAAQ,KAAK,KAAK;AAClB;;EAOF,MAAM,WAAW,iBAAiB,MAAM,GAAG;AAC3C,MAAI,aAAa,KAAM,YAAW,SAAS;AAC3C,YAAU,KAAK,KAAK;;AAGtB,QAAO;EAAE;EAAW;EAAS;;;;;;;;;;;;;;;;;;;;;;;;;AA0B/B,SAAgB,iBACd,QACA,QACe;CACf,MAAM,UAAU,mBAAmB,QAAQ,UAAU;AACrD,KAAI;AACF,SAAO,QAAQ,SAAS,EAAE,WAAW,MAAM,CAAC;UACrC,OAAO;AACd,gBAAc,QAAQ;AACtB,QAAM;;AAIR,KAAI,CAAC,WAAW,OAAO,CACrB,QAAO,cAAc,SAAS,QAAQ,KAAK;CAI7C,MAAM,SAAS,mBAAmB,QAAQ,SAAS;AACnD,KAAI;AACF,aAAW,QAAQ,OAAO;UACnB,OAAO;AACd,gBAAc,QAAQ;AACtB,QAAM;;AAER,QAAO,cAAc,SAAS,QAAQ,OAAO;;AAS/C,SAAS,cACP,SACA,QACA,QACe;AACf,KAAI;AACF,aAAW,SAAS,OAAO;UACpB,OAAO;AACd,MAAI,WAAW,KAAM,eAAc,QAAQ,QAAQ,MAAM;AACzD,gBAAc,QAAQ;AACtB,QAAM;;AAER,QAAO,cAAc,OAAO;;AAK9B,SAAS,cAAc,QAAgB,QAAgB,OAAsB;AAC3E,KAAI;AACF,aAAW,QAAQ,OAAO;SACpB;AACN,QAAM,IAAI,MACR,qBAAqB,OAAO,2CAA2C,OAAO,IAC9E,EAAE,OAAO,CACV;;;AAQL,SAAS,cAAc,MAAoC;AACzD,KAAI,SAAS,KAAM,QAAO;AAC1B,KAAI;AACF,SAAO,MAAM;GAAE,WAAW;GAAM,OAAO;GAAM,CAAC;AAC9C,SAAO;SACD;AACN,SAAO;;;AAOX,SAAS,mBAAmB,UAAkB,OAAuB;CACnE,IAAI,YAAY,GAAG,SAAS,GAAG;AAC/B,MAAK,IAAI,IAAI,GAAG,WAAW,UAAU,EAAE,KAAK,EAC1C,aAAY,GAAG,SAAS,GAAG,MAAM,GAAG;AAEtC,QAAO;;;;ACnLT,MAAM,qBAAqB;AAK3B,SAAS,0BAAkC;CACzC,MAAM,OAAO,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;CACpD,MAAM,aAAa;EACjB,KAAK,MAAM,SAAS;EACpB,KAAK,MAAM,MAAM,SAAS;EAC1B,KAAK,MAAM,MAAM,MAAM,SAAS;EACjC;AACD,MAAK,MAAM,OAAO,WAChB,KAAI,eAAe,IAAI,CAAC,SAAS,EAAG,QAAO;CAG7C,IAAI,MAAM;AACV,MAAK,IAAI,QAAQ,GAAG,QAAQ,GAAG,SAAS;EACtC,MAAM,YAAY,KAAK,KAAK,SAAS;AACrC,MAAI,eAAe,UAAU,CAAC,SAAS,EAAG,QAAO;AACjD,QAAM,QAAQ,IAAI;;AAEpB,OAAM,IAAI,MACR,uEACD;;AAOH,SAAS,yBAAyB,KAAa,YAA6B;AAC1E,KAAI,CAAC,IAAI,UAAU,IAAI,CAAC,SAAS,CAAE,QAAO;CAC1C,MAAM,MAAM,SAAS,KAAK,WAAW;AACrC,KAAI,IAAI,WAAW,KAAK,CAAE,QAAO;AACjC,QAAO,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,YAAY,QAAQ,WAAW,IAAI,CAAC;;AAGnE,SAAgB,sBAA+B;CAC7C,MAAM,SAAS,IAAI,QAAQ,SAAS,CAAC,YACnC,2CACD;AAED,QACG,QAAQ,UAAU,CAClB,YACC,sFACD,CACA,OAAO,oBAAoB,6BAA6B,mBAAmB,CAC3E,OAAO,eAAe,8CAA8C,CACpE,OAAO,OAAO,SAA2C;EACxD,MAAM,YAAY,yBAAyB;AAC3C,MAAI,eAAe,UAAU,CAAC,WAAW,GAAG;AAC1C,WAAQ,MAAM,sCAAsC;AACpD,WAAQ,KAAK,EAAE;;EAGjB,MAAM,aAAa,QAAQ,QAAQ,KAAK,EAAE,KAAK,IAAI;AACnD,MAAI,yBAAyB,QAAQ,KAAK,EAAE,WAAW,CACrD,SAAQ,IACN,GAAG,MAAM,OAAO,IAAI,CAAC,GAAG,MAAM,KAAK,KAAK,IAAI,CAAC,6HAEN,MAAM,KAAK,mBAAmB,CAAC,sBACvE;EAGH,MAAM,EAAE,WAAW,YAAY,MAAM,cAAc;GACjD;GACA;GACA,OAAO,QAAQ,KAAK,MAAM;GAC1B,kBAAkB,OAAO,SAAS;IAChC,MAAM,MAAM,MAAM,QAChB;KACE,MAAM;KACN,MAAM;KACN,SAAS,GAAG,MAAM,OAAO,KAAK,CAAC,qBAAqB,KAAK,IAAI;KAC7D,SAAS;KACV,EACD,EAAE,gBAAgB,QAAQ,KAAK,IAAI,EAAE,CACtC;AACD,WAAO,QAAQ,IAAI,UAAU;;GAI/B,aAAa,SAAS;AACpB,YAAQ,IACN,GAAG,MAAM,OAAO,IAAI,CAAC,6BAA6B,KAAK,4CACxD;;GAEJ,CAAC;AAEF,OAAK,MAAM,QAAQ,UACjB,SAAQ,IAAI,GAAG,MAAM,MAAM,IAAI,CAAC,GAAG,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG;AAEtE,OAAK,MAAM,QAAQ,QACjB,SAAQ,IAAI,GAAG,MAAM,IAAI,aAAa,KAAK,kBAAkB,GAAG;EAGlE,MAAM,QAAQ,CACZ,UAAU,SAAS,IAAI,GAAG,UAAU,OAAO,cAAc,MACzD,QAAQ,SAAS,IAAI,GAAG,QAAQ,OAAO,YAAY,KACpD,CAAC,OAAO,QAAQ;AACjB,UAAQ,IACN,KAAK,MAAM,KAAK,MAAM,KAAK,KAAK,IAAI,gBAAgB,CAAC,MAAM,aAC5D;AACD,MAAI,UAAU,SAAS,EACrB,SAAQ,IACN,MAAM,IAAI,wDAAwD,CACnE;GAEH;AAEJ,QAAO;;;;AChHT,SAAgB,qBAAqB,KAA0B;CAC7D,MAAM,MAAM,IAAI,QAAQ,QAAQ,CAAC,YAC/B,wEACD;AAED,KAAI,WAAW,kBAAkB,CAAC;AAClC,KAAI,WAAW,mBAAmB,CAAC;AACnC,KAAI,WAAW,mBAAmB,CAAC;AACnC,KAAI,WAAW,mBAAmB,CAAC;AACnC,KAAI,WAAW,mBAAmB,CAAC;AACnC,KAAI,WAAW,uBAAuB,CAAC;AACvC,KAAI,WAAW,qBAAqB,CAAC;AAErC,KAAI,QAAQ,WAAW,IAAI;;;;ACpB7B,MAAM,SAAsB;CAC1B,MAAM;CACN,SAAS;CACT,SAAS,KAAoB;AAC3B,uBAAqB,IAAI;;CAE5B"}