@fluid-app/fluid-cli-theme-dev 0.1.36 → 0.1.38
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.
- package/README.md +12 -0
- package/dist/index.mjs +422 -128
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["settingTypesJson.types","themes.listThemeResources","themes.updateThemeResource","themes.deleteThemeResource","themes.listApplicationThemes","themes.getApplicationTheme","themes.getApplicationTheme","themes.createApplicationTheme","themes.createApplicationTheme","themes.getApplicationTheme","themes.publishApplicationTheme","themes.getApplicationThemeAvailableThemeables"],"sources":["../../../platform/api-client-core/src/fetch-client.ts","../src/api.ts","../src/theme-config.ts","../src/plugin-state.ts","../src/theme/mime-type.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","../src/theme/dev-server/watcher.ts","../../../api-clients/themes/src/namespaces/v0.ts","../src/theme/stylesheet-keys.ts","../src/theme/format-error.ts","../src/theme/syncer.ts","../src/theme/dev-server/port-preflight.ts","../src/theme/dev-server/index.ts","../src/theme-picker.ts","../src/workspace.ts","../src/commands/dev.ts","../src/theme/shadow-repo.ts","../src/theme/merge-push.ts","../src/commands/push.ts","../src/theme/merge-pull.ts","../src/theme/legacy-migration.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 * Minimal, framework-agnostic fetch client for Fluid APIs\n * Compatible with fluid-admin patterns but usable standalone\n */\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 public readonly data: unknown;\n public readonly requestId?: string;\n\n constructor(\n message: string,\n status: number,\n data?: unknown,\n requestId?: string,\n ) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.data = data;\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: unknown;\n requestId?: string;\n } {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n data: this.data,\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 let data: Record<string, unknown>;\n try {\n data = 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 // Some Rails BFF endpoints return `{ error: { message, details } }`\n // instead of `{ message }` or `{ error_message }`. Fall through to\n // that shape last so callers always surface the real reason.\n const nestedError =\n typeof data.error === \"object\" && data.error !== null\n ? (data.error as { message?: unknown }).message\n : undefined;\n const msg =\n (data.message as string | undefined) ||\n (data.error_message as string | undefined) ||\n (typeof nestedError === \"string\" ? nestedError : undefined);\n throw new ApiError(\n msg || `${method} request failed`,\n response.status,\n data.errors || data,\n headerRequestId ?? getRequestIdFromJsonBody(data),\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}\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\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 return JSON.parse(raw) as ThemeConfig;\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}\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","","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 {\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 = 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 { readdirSync, statSync } from \"node:fs\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport { ThemeFile } from \"./file.js\";\nimport { FluidIgnore } from \"./fluid-ignore.js\";\n\nconst THEME_MARKERS = [\"templates\", \"assets\", \"config\"];\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 THEME_MARKERS.some((m) => {\n try {\n return statSync(join(this.root, m)).isDirectory();\n } catch {\n return false;\n }\n });\n }\n\n files(): ThemeFile[] {\n return this.glob(this.root).filter(\n (f) => !this.ignore.ignore(f.relativePath),\n );\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 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","import { relative } from \"node:path\";\nimport chokidar from \"chokidar\";\nimport type { ThemeRoot } from \"../root.js\";\nimport type { ThemeFile } from \"../file.js\";\n\nexport type FileChangeHandler = (\n modified: ThemeFile[],\n added: ThemeFile[],\n removed: ThemeFile[],\n) => Promise<void>;\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 = relative(root.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 pending = pending.then(fn).catch(() => {});\n };\n\n watcher.on(\"change\", (filePath) => {\n const rel = relative(root.root, filePath);\n if (root.ignore.ignore(rel)) return;\n enqueue(() => handler([root.file(filePath)], [], []));\n });\n\n watcher.on(\"add\", (filePath) => {\n const rel = relative(root.root, filePath);\n if (root.ignore.ignore(rel)) return;\n enqueue(() => handler([], [root.file(filePath)], []));\n });\n\n watcher.on(\"unlink\", (filePath) => {\n enqueue(() => handler([], [], [root.file(filePath)]));\n });\n\n return () => watcher.close();\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 * 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","// 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 { 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 { sep } from \"node:path\";\nimport type { ApiClient } from \"../api.js\";\nimport type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport { themes, type components } from \"@fluid-app/themes-api-client\";\nimport { isStylesheetKey } from \"./stylesheet-keys.js\";\nimport { formatError } from \"./format-error.js\";\n\ntype RemoteResource = components[\"schemas\"][\"ApplicationThemeResource\"];\n\nexport interface SyncResult {\n uploaded: number;\n downloaded: 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 checksums = new Map<string, string>();\n private lastKnownRemoteSha: string | null = null;\n\n constructor(\n private api: ApiClient,\n private themeId: number,\n private themeRoot: ThemeRoot,\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 }\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 for (const r of resources) {\n if (r.key && r.checksum) this.checksums.set(r.key, r.checksum);\n }\n for (const key of this.checksums.keys()) {\n if (this.checksums.has(`${key}.liquid`)) this.checksums.delete(key);\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.checksums.keys()];\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 // ─── Upload ───────────────────────────────────────────────────────────────\n\n async uploadFile(file: ThemeFile, baseSha?: string | null): Promise<void> {\n if (file.isText) {\n await this.putResource(\n { key: file.relativePath, content: file.read() },\n baseSha,\n );\n } else {\n await this.uploadBinaryFile(file, baseSha);\n }\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<void> {\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 { content_version_sha?: string };\n if (response.content_version_sha) {\n this.lastKnownRemoteSha = response.content_version_sha;\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 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<void> {\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 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\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 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 throw this.rethrowIfConflict(e);\n }\n this.checksums.delete(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 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 // ─── Full Upload ──────────────────────────────────────────────────────────\n\n async uploadTheme(\n opts: {\n delete?: boolean;\n validate?: 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 } = {},\n ): Promise<SyncResult> {\n await this.fetchChecksums();\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 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 const localFiles = this.themeRoot.files();\n const result: SyncResult = {\n uploaded: 0,\n deleted: 0,\n downloaded: 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 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 // `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 const toDelete = this.remoteKeys().filter((k) => !localPaths.has(k));\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 result: SyncResult & { skipped: number } = {\n uploaded: 0,\n deleted: 0,\n downloaded: 0,\n skipped: 0,\n errors: [],\n validationFailed: false,\n };\n\n let done = 0;\n for (const resource of resources) {\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 (!file.absolutePath.startsWith(this.themeRoot.root + sep)) {\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 const { unlinkSync } = await import(\"node:fs\");\n unlinkSync(file.absolutePath);\n result.deleted++;\n } catch {\n // ignore\n }\n }\n }\n\n return result;\n }\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 { Syncer } from \"../syncer.js\";\nimport type { ThemeRoot } from \"../root.js\";\nimport type { ApiClient } from \"../../api.js\";\nimport { formatPortConflictMessage } from \"./port-preflight.js\";\n\nexport interface DevServerOptions {\n host: string;\n port: number;\n reloadMode: \"full-page\" | \"off\";\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\n const pendingUpdates = new Set<string>();\n\n // ── Initial sync ─────────────────────────────────────────────────────────\n console.log(`\\nSyncing theme ${theme.name} (#${theme.id})…`);\n const syncResult = await syncer.uploadTheme({\n delete: true,\n validate: opts.validate,\n onProgress: (done, total) => {\n process.stdout.write(`\\r Uploading ${done}/${total} files…`);\n },\n });\n process.stdout.write(\"\\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 for (const e of syncResult.errors) console.error(` ${e}`);\n }\n\n // ── File watcher ─────────────────────────────────────────────────────────\n const stopWatcher = watchTheme(\n themeRoot,\n async (modified, added, removed) => {\n const changed = [...modified, ...added];\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 await syncer.uploadFile(file);\n } catch (e) {\n console.error(\n `\\n[Watcher] Upload failed: ${file.relativePath}: ${e}`,\n );\n } finally {\n pendingUpdates.delete(file.relativePath);\n }\n }\n\n for (const file of removed) {\n try {\n await syncer.deleteRemoteFile(file.relativePath);\n } catch {\n // ignore\n }\n }\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 );\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 res.writeHead(502);\n res.end(\"Bad Gateway\");\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 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} from \"../plugin-state.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { startDevServer } from \"../theme/dev-server/index.js\";\nimport {\n checkPortAvailable,\n PortInUseError,\n} from \"../theme/dev-server/port-preflight.js\";\nimport { 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\nasync function ensureDevTheme(\n api: ReturnType<typeof createApiClient>,\n projectKey: string,\n identifier?: string,\n): Promise<ApplicationTheme> {\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;\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 if (stored) {\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, { id: existing.id, name: existing.name });\n return existing;\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 body = await themes.createApplicationTheme(api, {\n application_theme: { name, status: \"development\" },\n });\n const theme = body.application_theme;\n setDevTheme(projectKey, { id: theme.id, name: theme.name });\n console.log(`Created dev theme: ${theme.name} (#${theme.id})`);\n return theme;\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. We deliberately do NOT\n // fall back to `.fluid-theme.json`'s themeId — that points at whatever\n // was pulled (often the active/production theme), and the dev server's\n // `delete: true` sync would overwrite it in place.\n const projectKey = devThemeKey(company, themeRoot.root);\n const theme = opts.theme\n ? await ensureDevTheme(api, projectKey, opts.theme)\n : await ensureDevTheme(api, projectKey);\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 { host: opts.host, port, reloadMode, validate: !opts.force },\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 { 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 const { stdout } = await this.git([\"ls-tree\", \"-r\", \"HEAD\", \"--name-only\"]);\n return stdout\n .toString(\"utf8\")\n .split(\"\\n\")\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 for (const { path, sha } of files) {\n await this.git(\n [\"update-index\", \"--add\", \"--cacheinfo\", `100644,${sha},${path}`],\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 async merge3(\n base: Buffer | null,\n local: Buffer,\n remote: Buffer,\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 \"-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 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 type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport { ShadowRepo, readIfExists } from \"./shadow-repo.js\";\nimport { isStylesheetKey } from \"./stylesheet-keys.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\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 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 if (await shadow.hasHead()) {\n for (const key of await listHeadPaths(shadow)) {\n if (localByKey.has(key)) continue;\n if (isStylesheetKey(key)) continue; // hidden from the API surface\n deleted.push(key);\n }\n }\n\n return { changed, deleted };\n}\n\n/**\n * Read every path recorded under HEAD's tree. Used to detect local\n * deletions (paths in HEAD, absent from the working tree). Implemented\n * with `git ls-tree -r HEAD --name-only` piped through the shadow\n * repo's plumbing.\n */\nasync function listHeadPaths(shadow: ShadowRepo): Promise<string[]> {\n return shadow.headPaths();\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\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 for (const file of themeRoot.files()) {\n if (!file.exists) continue;\n const buf = file.isText ? Buffer.from(file.read()) : file.readBinary();\n entries.push({ path: file.relativePath, sha: await shadow.writeBlob(buf) });\n }\n if (entries.length > 0) await shadow.commitState(entries, message);\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 { ThemeRoot } from \"../theme/root.js\";\nimport { PushConflictError, Syncer } from \"../theme/syncer.js\";\nimport { ShadowRepo } from \"../theme/shadow-repo.js\";\nimport {\n commitPushedState,\n diffAgainstShadow,\n findUnresolvedConflicts,\n} from \"../theme/merge-push.js\";\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\";\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 schema 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(\"--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 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 const localFiles = themeRoot.files().filter((f) => f.exists);\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.\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 process.exit(1);\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\n if (changed.length === 0 && deleted.length === 0) {\n console.log(\"Nothing to push — local matches the last synced state.\");\n return;\n }\n\n const spinner = ora(`Pushing to ${theme.name} (#${theme.id})…`).start();\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 // Refuse to push into an existing theme without a valid\n // baseline — otherwise the CLI silently clobbers whatever the\n // server 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 && !baseSha) {\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, 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 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 // Optional schema validation (unchanged from the old flow —\n // still catches broken Liquid before we ship it).\n if (!opts.force) {\n const errors: string[] = [];\n for (const file of changed) {\n if (!file.isLiquid) continue;\n for (const d of file.validateSchema()) {\n if (d.severity === \"error\") {\n errors.push(`${file.relativePath}: ${d.message}`);\n }\n }\n }\n if (errors.length > 0) {\n spinner.fail(\n `Schema validation failed (${errors.length} error(s)). Use --force to skip.`,\n );\n for (const err of errors) console.error(` ${err}`);\n process.exit(1);\n }\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) {\n spinner.warn(`Pushed with ${errors.length} error(s).`);\n for (const err of errors) console.error(` ${err}`);\n } else {\n spinner.succeed(\n `Pushed ${uploaded} file(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 await commitPushedState(\n themeRoot,\n shadow,\n `push @ ${new Date().toISOString()}`,\n );\n }\n\n if (config) {\n writeThemeConfig(themeRoot.root, {\n themeId: theme.id,\n themeName: theme.name,\n company: config.company,\n baseSha: baseSha ?? undefined,\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 }\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 { ShadowRepo, looksBinary, readIfExists } from \"./shadow-repo.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport { isStylesheetKey } from \"./stylesheet-keys.js\";\n\ntype RemoteResource = components[\"schemas\"][\"ApplicationThemeResource\"];\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 /** 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 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 deleted: 0,\n skipped: 0,\n errors: [],\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 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 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 file.write(remoteBuf);\n 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 file.write(remoteBuf);\n 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 file.write(remoteBuf);\n result.written++;\n continue;\n }\n\n if (baseBuf && remoteBuf.equals(baseBuf)) {\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. Take remote and flag it so\n // the user can compare against the shadow repo (`git show HEAD:...`)\n // and reapply any local intent by hand.\n file.write(remoteBuf);\n result.conflicts.push(`${key} (binary — kept remote)`);\n continue;\n }\n\n const { merged, hasConflicts } = await shadow.merge3(\n baseBuf,\n localBuf,\n remoteBuf,\n );\n file.write(merged);\n if (hasConflicts) result.conflicts.push(key);\n else result.merged++;\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 const commitEntries: Array<{ path: string; sha: string }> = [];\n for (const [key, buf] of remoteContent) {\n commitEntries.push({ path: key, sha: await shadow.writeBlob(buf) });\n }\n for (const key of remoteKeys) {\n if (remoteContent.has(key)) continue; // downloaded fine — already added\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 if (commitEntries.length > 0) {\n await shadow.commitState(\n commitEntries,\n `pull @ ${new Date().toISOString()}`,\n );\n }\n\n return result;\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 { 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 { 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 { selectTheme, findTheme } from \"../theme-picker.js\";\nimport { findWorkspace, resolveThemeRootFromCwd } from \"../workspace.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 .action(\n async (opts: {\n theme?: string;\n nodelete?: boolean;\n root?: string;\n yes?: boolean;\n force?: boolean;\n }) => {\n requireToken();\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 const spinner = ora(`Pulling ${theme.name} (#${theme.id})…`).start();\n const resources = await syncer.downloadAll();\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 onProgress: (done, total) => {\n spinner.text = `Downloading ${done}/${total} files…`;\n },\n });\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 (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 } 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 });\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 { 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 }\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","themes.listThemeResources","themes.updateThemeResource","themes.deleteThemeResource","themes.listApplicationThemes","themes.getApplicationTheme","themes.getApplicationTheme","themes.createApplicationTheme","themes.createApplicationTheme","themes.getApplicationTheme","themes.publishApplicationTheme","themes.getApplicationThemeAvailableThemeables"],"sources":["../../../platform/api-client-core/src/fetch-client.ts","../src/api.ts","../src/theme-config.ts","../src/plugin-state.ts","../src/theme/mime-type.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/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-picker.ts","../src/workspace.ts","../src/commands/dev.ts","../src/theme/shadow-repo.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 * Minimal, framework-agnostic fetch client for Fluid APIs\n * Compatible with fluid-admin patterns but usable standalone\n */\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 public readonly data: unknown;\n public readonly requestId?: string;\n\n constructor(\n message: string,\n status: number,\n data?: unknown,\n requestId?: string,\n ) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.data = data;\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: unknown;\n requestId?: string;\n } {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n data: this.data,\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 let data: Record<string, unknown>;\n try {\n data = 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 // Some Rails BFF endpoints return `{ error: { message, details } }`\n // instead of `{ message }` or `{ error_message }`. Fall through to\n // that shape last so callers always surface the real reason.\n const nestedError =\n typeof data.error === \"object\" && data.error !== null\n ? (data.error as { message?: unknown }).message\n : undefined;\n const msg =\n (data.message as string | undefined) ||\n (data.error_message as string | undefined) ||\n (typeof nestedError === \"string\" ? nestedError : undefined);\n throw new ApiError(\n msg || `${method} request failed`,\n response.status,\n data.errors || data,\n headerRequestId ?? getRequestIdFromJsonBody(data),\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}\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\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 return JSON.parse(raw) as ThemeConfig;\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}\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","","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 {\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 = 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 { readdirSync, statSync } from \"node:fs\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport { ThemeFile } from \"./file.js\";\nimport { FluidIgnore } from \"./fluid-ignore.js\";\n\nconst THEME_MARKERS = [\"templates\", \"assets\", \"config\"];\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 THEME_MARKERS.some((m) => {\n try {\n return statSync(join(this.root, m)).isDirectory();\n } catch {\n return false;\n }\n });\n }\n\n files(): ThemeFile[] {\n return this.glob(this.root).filter(\n (f) => !this.ignore.ignore(f.relativePath),\n );\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 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 * 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 } 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) => Promise<void>;\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 = relative(root.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 = relative(root.root, filePath);\n if (root.ignore.ignore(rel)) return;\n enqueue(() => handler([root.file(filePath)], [], []));\n });\n\n watcher.on(\"add\", (filePath) => {\n const rel = relative(root.root, filePath);\n if (root.ignore.ignore(rel)) return;\n enqueue(() => handler([], [root.file(filePath)], []));\n });\n\n watcher.on(\"unlink\", (filePath) => {\n enqueue(() => handler([], [], [root.file(filePath)]));\n });\n\n return () => watcher.close();\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 { sep } from \"node:path\";\nimport type { ApiClient } from \"../api.js\";\nimport type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport { themes, type components } from \"@fluid-app/themes-api-client\";\nimport { isStylesheetKey } from \"./stylesheet-keys.js\";\nimport { formatError } from \"./format-error.js\";\n\ntype RemoteResource = components[\"schemas\"][\"ApplicationThemeResource\"];\n\nexport interface SyncResult {\n uploaded: number;\n downloaded: 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 checksums = new Map<string, string>();\n private lastKnownRemoteSha: string | null = null;\n\n constructor(\n private api: ApiClient,\n private themeId: number,\n private themeRoot: ThemeRoot,\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 }\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 for (const r of resources) {\n if (r.key && r.checksum) this.checksums.set(r.key, r.checksum);\n }\n for (const key of this.checksums.keys()) {\n if (this.checksums.has(`${key}.liquid`)) this.checksums.delete(key);\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.checksums.keys()];\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 // ─── Upload ───────────────────────────────────────────────────────────────\n\n /**\n * Uploads one file. Resolves with the exact text content that was\n * sent to the server (null for binary files) so callers can run\n * diagnostics against the same bytes instead of re-reading a file\n * that may have changed on disk while the request was in flight.\n */\n async uploadFile(\n file: ThemeFile,\n baseSha?: string | null,\n ): Promise<string | null> {\n if (file.isText) {\n const content = file.read();\n await this.putResource({ key: file.relativePath, content }, baseSha);\n return content;\n }\n await this.uploadBinaryFile(file, baseSha);\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<void> {\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 { content_version_sha?: string };\n if (response.content_version_sha) {\n this.lastKnownRemoteSha = response.content_version_sha;\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 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<void> {\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 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\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 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 throw this.rethrowIfConflict(e);\n }\n this.checksums.delete(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 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 // ─── Full Upload ──────────────────────────────────────────────────────────\n\n async uploadTheme(\n opts: {\n delete?: boolean;\n validate?: 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 } = {},\n ): Promise<SyncResult> {\n await this.fetchChecksums();\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 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 const localFiles = this.themeRoot.files();\n const result: SyncResult = {\n uploaded: 0,\n deleted: 0,\n downloaded: 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 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 // `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 const toDelete = this.remoteKeys().filter((k) => !localPaths.has(k));\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 result: SyncResult & { skipped: number } = {\n uploaded: 0,\n deleted: 0,\n downloaded: 0,\n skipped: 0,\n errors: [],\n validationFailed: false,\n };\n\n let done = 0;\n for (const resource of resources) {\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 (!file.absolutePath.startsWith(this.themeRoot.root + sep)) {\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 const { unlinkSync } = await import(\"node:fs\");\n unlinkSync(file.absolutePath);\n result.deleted++;\n } catch {\n // ignore\n }\n }\n }\n\n return result;\n }\n}\n","/**\n * Heuristic-only check for obviously unbalanced liquid delimiters\n * (`{% %}` and `{{ }}`). This is NOT a liquid parser — it just counts\n * delimiter occurrences. 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 * 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 const tagOpens = content.match(/\\{%/g)?.length ?? 0;\n const tagCloses = content.match(/%\\}/g)?.length ?? 0;\n const outputOpens = content.match(/\\{\\{/g)?.length ?? 0;\n const outputCloses = content.match(/\\}\\}/g)?.length ?? 0;\n\n return tagOpens !== tagCloses || outputOpens !== outputCloses;\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 { Syncer } from \"../syncer.js\";\nimport { hasUnbalancedLiquidDelimiters } from \"../liquid-delimiters.js\";\nimport type { ThemeRoot } from \"../root.js\";\nimport type { ApiClient } from \"../../api.js\";\nimport { formatPortConflictMessage } from \"./port-preflight.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}\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\n const pendingUpdates = new Set<string>();\n\n // ── Initial sync ─────────────────────────────────────────────────────────\n console.log(`\\nSyncing theme ${theme.name} (#${theme.id})…`);\n const syncResult = await syncer.uploadTheme({\n delete: true,\n validate: opts.validate,\n onProgress: (done, total) => {\n process.stdout.write(`\\r Uploading ${done}/${total} files…`);\n },\n });\n process.stdout.write(\"\\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 for (const e of syncResult.errors) console.error(` ${e}`);\n }\n\n // ── File watcher ─────────────────────────────────────────────────────────\n const stopWatcher = watchTheme(\n themeRoot,\n async (modified, added, removed) => {\n const changed = [...modified, ...added];\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(file);\n console.log(` ✓ synced ${file.relativePath} (${timestamp()})`);\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 } catch (e) {\n console.error(\n `\\n[Watcher] Upload failed: ${file.relativePath}: ${e}`,\n );\n } finally {\n pendingUpdates.delete(file.relativePath);\n }\n }\n\n for (const file of removed) {\n try {\n await syncer.deleteRemoteFile(file.relativePath);\n console.log(` ✓ removed ${file.relativePath}`);\n } catch {\n // ignore\n }\n }\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 );\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 res.writeHead(502);\n res.end(\"Bad Gateway\");\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 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} from \"../plugin-state.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { startDevServer } from \"../theme/dev-server/index.js\";\nimport {\n checkPortAvailable,\n PortInUseError,\n} from \"../theme/dev-server/port-preflight.js\";\nimport { 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\nasync function ensureDevTheme(\n api: ReturnType<typeof createApiClient>,\n projectKey: string,\n identifier?: string,\n): Promise<ApplicationTheme> {\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;\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 if (stored) {\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, { id: existing.id, name: existing.name });\n return existing;\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 body = await themes.createApplicationTheme(api, {\n application_theme: { name, status: \"development\" },\n });\n const theme = body.application_theme;\n setDevTheme(projectKey, { id: theme.id, name: theme.name });\n console.log(`Created dev theme: ${theme.name} (#${theme.id})`);\n return theme;\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. We deliberately do NOT\n // fall back to `.fluid-theme.json`'s themeId — that points at whatever\n // was pulled (often the active/production theme), and the dev server's\n // `delete: true` sync would overwrite it in place.\n const projectKey = devThemeKey(company, themeRoot.root);\n const theme = opts.theme\n ? await ensureDevTheme(api, projectKey, opts.theme)\n : await ensureDevTheme(api, projectKey);\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 { host: opts.host, port, reloadMode, validate: !opts.force },\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 { 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 const { stdout } = await this.git([\"ls-tree\", \"-r\", \"HEAD\", \"--name-only\"]);\n return stdout\n .toString(\"utf8\")\n .split(\"\\n\")\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 for (const { path, sha } of files) {\n await this.git(\n [\"update-index\", \"--add\", \"--cacheinfo\", `100644,${sha},${path}`],\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 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 type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport { ShadowRepo, readIfExists } from \"./shadow-repo.js\";\nimport { isStylesheetKey } from \"./stylesheet-keys.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\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 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 if (await shadow.hasHead()) {\n for (const key of await listHeadPaths(shadow)) {\n if (localByKey.has(key)) continue;\n if (isStylesheetKey(key)) continue; // hidden from the API surface\n deleted.push(key);\n }\n }\n\n return { changed, deleted };\n}\n\n/**\n * Read every path recorded under HEAD's tree. Used to detect local\n * deletions (paths in HEAD, absent from the working tree). Implemented\n * with `git ls-tree -r HEAD --name-only` piped through the shadow\n * repo's plumbing.\n */\nasync function listHeadPaths(shadow: ShadowRepo): Promise<string[]> {\n return shadow.headPaths();\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 for (const file of themeRoot.files()) {\n if (!file.exists) continue;\n const buf = file.isText ? Buffer.from(file.read()) : file.readBinary();\n entries.push({ path: file.relativePath, sha: await shadow.writeBlob(buf) });\n }\n if (entries.length > 0) await shadow.commitState(entries, message);\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\";\n\n/**\n * User-stamped shadow-repo commit subjects, mirroring the Mist CLI's\n * push stamp (packages/cli/mist/src/commands/push.ts): name +\n * description first so `git log --oneline` stays scannable, the long\n * UUID last where truncation can't eat the prose. Format:\n *\n * Mike Tingey: Push from Fluid CLI · 2026-07-22 05:10:33 (0182d21e-…)\n *\n * When the actor is unknown (offline, /api/me down, token weirdness)\n * we fall back to the pre-existing machine wording (`push @ <iso>`)\n * so downstream consumers — Mist Desktop's History popover parses\n * these subjects — keep working unchanged.\n */\n\nexport type SyncVerb =\n | \"Push\"\n | \"Pull\"\n | \"Baseline from server\"\n | \"Snapshot before pull\";\n\n/** Legacy (actor-less) wording per verb. `Push`/`Pull` are byte-\n * compatible with what the CLI has always written. */\nconst LEGACY_WORDING: Record<SyncVerb, string> = {\n Push: \"push\",\n Pull: \"pull\",\n \"Baseline from server\": \"baseline from server\",\n \"Snapshot before pull\": \"snapshot before pull\",\n};\n\n/** Human base message per verb, used in the stamped format. */\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\nexport interface SyncActor {\n /** Display name: full_name → email → `user-<id>` (Mist's ordering). */\n name: string;\n /** The user's public UUID, appended as a ` (uuid)` suffix. */\n publicId: string | null;\n}\n\nexport function syncCommitSubject(\n verb: SyncVerb,\n actor: SyncActor | null,\n when: Date = new Date(),\n): string {\n if (!actor) return `${LEGACY_WORDING[verb]} @ ${when.toISOString()}`;\n const stamp = when.toISOString().slice(0, 19).replace(\"T\", \" \");\n const idSuffix = actor.publicId ? ` (${actor.publicId})` : \"\";\n return `${actor.name}: ${BASE_MESSAGE[verb]} · ${stamp}${idSuffix}`;\n}\n\ninterface MeUser {\n id?: number;\n public_id?: string | null;\n full_name?: string | null;\n first_name?: string | null;\n last_name?: string | null;\n email?: string | null;\n}\n\ninterface MeResponse extends MeUser {\n user?: MeUser;\n}\n\n/** Resolve a `/api/me` body to a commit actor. Exported for tests.\n * Name fallback ordering matches the Mist CLI: full_name (or a\n * first+last composition) → email → `user-<id>`; null when none of\n * those exist — the caller then uses the legacy wording. */\nexport function actorFromMe(raw: MeResponse): SyncActor | null {\n const user = raw.user ?? raw;\n const composed = [user.first_name, user.last_name]\n .filter(Boolean)\n .join(\" \")\n .trim();\n const name =\n (user.full_name && user.full_name.trim()) ||\n (composed.length > 0 ? composed : null) ||\n (user.email && user.email.trim()) ||\n (typeof user.id === \"number\" && Number.isFinite(user.id)\n ? `user-${user.id}`\n : null);\n if (!name) return null;\n const publicId = user.public_id && user.public_id.trim();\n return { name, publicId: publicId || null };\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 try {\n const raw = await Promise.race([\n api.get<MeResponse>(\"/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 { 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 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 { 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\";\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 schema 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\n if (changed.length === 0 && deleted.length === 0) {\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 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 // Optional schema validation (unchanged from the old flow —\n // still catches broken Liquid before we ship it).\n if (!opts.force) {\n const errors: string[] = [];\n for (const file of changed) {\n if (!file.isLiquid) continue;\n for (const d of file.validateSchema()) {\n if (d.severity === \"error\") {\n errors.push(`${file.relativePath}: ${d.message}`);\n }\n }\n }\n if (errors.length > 0) {\n spinner.fail(\n `Schema validation failed (${errors.length} error(s)). Use --force to skip.`,\n );\n for (const err of errors) console.error(` ${err}`);\n process.exit(1);\n }\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) {\n spinner.warn(`Pushed with ${errors.length} error(s).`);\n for (const err of errors) console.error(` ${err}`);\n } else {\n spinner.succeed(\n `Pushed ${uploaded} file(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 await commitPushedState(\n themeRoot,\n shadow,\n syncCommitSubject(\"Push\", await getActor()),\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 });\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 });\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 }\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 { 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\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 * 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<{ file: ThemeFile; content: Buffer }> = [];\n const write = (file: ThemeFile, content: Buffer): void => {\n if (input.resolve) pendingWrites.push({ file, content });\n else file.write(content);\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 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 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(file, remoteBuf);\n 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(file, remoteBuf);\n 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(file, remoteBuf);\n result.written++;\n continue;\n }\n\n if (baseBuf && remoteBuf.equals(baseBuf)) {\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(file, remoteBuf);\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 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(file, favored.merged);\n result.autoResolved.push(key);\n continue;\n }\n write(file, merged);\n if (hasConflicts) result.conflicts.push(key);\n else result.merged++;\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 (result.autoResolved.length > 0) {\n await commitPushedState(\n themeRoot,\n shadow,\n syncCommitSubject(\"Snapshot before pull\", input.actor ?? null),\n );\n }\n for (const { file, content } of pendingWrites) file.write(content);\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 const commitEntries: Array<{ path: string; sha: string }> = [];\n for (const [key, buf] of remoteContent) {\n commitEntries.push({ path: key, sha: await shadow.writeBlob(buf) });\n }\n for (const key of remoteKeys) {\n if (remoteContent.has(key)) continue; // downloaded fine — already added\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 if (commitEntries.length > 0) {\n await shadow.commitState(\n commitEntries,\n syncCommitSubject(\"Pull\", input.actor ?? null),\n );\n }\n\n return result;\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\";\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();\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 resolve: resolveSide,\n actor: await actorPromise,\n onProgress: (done, total) => {\n spinner.text = `Downloading ${done}/${total} files…`;\n },\n });\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 (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 } 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 });\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 { 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 }\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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"}
|