@fluid-app/fluid-cli-theme-dev 0.1.21 → 0.1.23

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.
Files changed (48) hide show
  1. package/README.md +14 -0
  2. package/dist/index.mjs +155 -2
  3. package/dist/index.mjs.map +1 -1
  4. package/package.json +8 -4
  5. package/.turbo/turbo-build.log +0 -16
  6. package/.turbo/turbo-typecheck.log +0 -4
  7. package/jest.config.cjs +0 -21
  8. package/jest.mocks/fluid-cli.ts +0 -33
  9. package/src/__tests__/plugin-state.test.ts +0 -186
  10. package/src/api.ts +0 -28
  11. package/src/commands/dev.ts +0 -186
  12. package/src/commands/init.ts +0 -51
  13. package/src/commands/lint.ts +0 -186
  14. package/src/commands/navigate.ts +0 -259
  15. package/src/commands/pull.ts +0 -242
  16. package/src/commands/push.ts +0 -220
  17. package/src/commands/theme.ts +0 -23
  18. package/src/index.ts +0 -12
  19. package/src/plugin-state.ts +0 -171
  20. package/src/theme/dev-server/hot-reload.ts +0 -65
  21. package/src/theme/dev-server/index.ts +0 -145
  22. package/src/theme/dev-server/proxy.ts +0 -125
  23. package/src/theme/dev-server/sse.ts +0 -43
  24. package/src/theme/dev-server/watcher.ts +0 -54
  25. package/src/theme/file.ts +0 -104
  26. package/src/theme/fluid-ignore.ts +0 -64
  27. package/src/theme/mime-type.ts +0 -45
  28. package/src/theme/root.ts +0 -54
  29. package/src/theme/syncer.ts +0 -338
  30. package/src/theme-config.ts +0 -34
  31. package/src/theme-picker.ts +0 -164
  32. package/src/workspace.ts +0 -71
  33. package/tsconfig.json +0 -10
  34. package/tsdown.config.ts +0 -19
  35. /package/{skills → dist/skills}/themes-review/SKILL.md +0 -0
  36. /package/{skills → dist/skills}/themes-review/references/blocks-vs-sections.md +0 -0
  37. /package/{skills → dist/skills}/themes-review/references/css-js-hygiene.md +0 -0
  38. /package/{skills → dist/skills}/themes-review/references/dead-code.md +0 -0
  39. /package/{skills → dist/skills}/themes-review/references/dynamism.md +0 -0
  40. /package/{skills → dist/skills}/themes-review/references/editor-attributes.md +0 -0
  41. /package/{skills → dist/skills}/themes-review/references/examples.md +0 -0
  42. /package/{skills → dist/skills}/themes-review/references/fairshare-attributes.md +0 -0
  43. /package/{skills → dist/skills}/themes-review/references/global-settings.md +0 -0
  44. /package/{skills → dist/skills}/themes-review/references/liquid-correctness.md +0 -0
  45. /package/{skills → dist/skills}/themes-review/references/navigation.md +0 -0
  46. /package/{skills → dist/skills}/themes-review/references/performance.md +0 -0
  47. /package/{skills → dist/skills}/themes-review/references/security-accessibility.md +0 -0
  48. /package/{skills → dist/skills}/themes-review/references/setting-types.md +0 -0
@@ -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/syncer.ts","../src/theme/dev-server/index.ts","../src/theme-picker.ts","../src/workspace.ts","../src/commands/dev.ts","../src/commands/push.ts","../src/commands/pull.ts","../src/commands/lint.ts","../src/commands/init.ts","../src/commands/navigate.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 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/** 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","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\";\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\nexport class Syncer {\n private checksums = new Map<string, string>();\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 const body = await themes.listThemeResources(this.api, this.themeId);\n this.updateChecksums(body.application_theme_resources ?? []);\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): Promise<void> {\n if (file.isText) {\n await themes.updateThemeResource(this.api, this.themeId, {\n application_theme_resource: {\n key: file.relativePath,\n content: file.read(),\n },\n });\n } else {\n await this.uploadBinaryFile(file);\n }\n }\n\n private async uploadBinaryFile(file: ThemeFile): 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\n await themes.updateThemeResource(this.api, this.themeId, {\n application_theme_resource: {\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 });\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(relativePath: string): Promise<void> {\n await themes.deleteThemeResource(this.api, this.themeId, {\n application_theme_resource: { key: relativePath },\n });\n this.checksums.delete(relativePath);\n }\n\n // ─── Download ─────────────────────────────────────────────────────────────\n\n async downloadAll(): Promise<RemoteResource[]> {\n const body = await themes.listThemeResources(this.api, this.themeId);\n const resources = body.application_theme_resources ?? [];\n this.updateChecksums(resources);\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 } = {},\n ): Promise<SyncResult> {\n await this.fetchChecksums();\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);\n result.uploaded++;\n } catch (e) {\n result.errors.push(`Upload ${file.relativePath}: ${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);\n result.deleted++;\n } catch (e) {\n result.errors.push(`Delete ${key}: ${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}: ${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)) {\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\n return result;\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\";\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 await new Promise<void>((resolve, reject) => {\n server.listen(opts.port, opts.host, () => resolve());\n server.on(\"error\", reject);\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 { 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 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 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 { 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\";\n\n/**\n * Detect files where the remote has changed since the last pull,\n * and we also have local changes (i.e. we'd overwrite someone else's work).\n */\nfunction detectRemoteDrift(\n storedChecksums: Record<string, string>,\n remoteChecksums: Record<string, string>,\n themeRoot: ThemeRoot,\n): string[] {\n const conflicts: string[] = [];\n for (const [key, storedChecksum] of Object.entries(storedChecksums)) {\n const remoteChecksum = remoteChecksums[key];\n if (remoteChecksum === undefined) continue;\n if (remoteChecksum === storedChecksum) continue; // remote unchanged since pull\n\n // Remote changed — check if we also have this file locally (and it differs)\n const file = themeRoot.file(key);\n if (!file.exists) continue;\n const localChecksum = file.checksum();\n if (localChecksum === remoteChecksum) continue; // local matches remote already\n\n conflicts.push(key);\n }\n return conflicts;\n}\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(\"-f, --force\", \"Skip schema validation\")\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 // 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 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 // Use .fluid-theme.json as the default\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 // Check for remote drift if we have stored checksums\n if (config?.checksums && !opts.force) {\n const driftSpinner = ora(\"Checking for remote changes…\").start();\n const driftSyncer = new Syncer(api, theme.id, themeRoot);\n await driftSyncer.fetchChecksums();\n const remoteChecksums = driftSyncer.remoteChecksums();\n const conflicts = detectRemoteDrift(\n config.checksums,\n remoteChecksums,\n themeRoot,\n );\n driftSpinner.stop();\n\n if (conflicts.length > 0) {\n console.log(\n chalk.yellow(\n `\\n⚠ ${conflicts.length} file(s) changed on remote since last pull:\\n`,\n ),\n );\n for (const key of conflicts) {\n console.log(` ${key}`);\n }\n console.log();\n\n const { resolution } = await prompts(\n {\n type: \"select\",\n name: \"resolution\",\n message: \"How do you want to handle this?\",\n choices: [\n {\n title: \"Push anyway (overwrite remote changes)\",\n value: \"push\",\n },\n {\n title: \"Pull first, then push\",\n value: \"pull-first\",\n },\n { title: \"Abort\", value: \"abort\" },\n ],\n },\n { onCancel: () => process.exit(130) },\n );\n\n if (resolution === \"abort\") {\n console.log(\"Aborted.\");\n process.exit(0);\n }\n if (resolution === \"pull-first\") {\n console.log(\n `Run ${chalk.cyan(\"fluid theme pull\")} first, then push again.`,\n );\n process.exit(0);\n }\n }\n }\n\n const syncer = new Syncer(api, theme.id, themeRoot);\n const spinner = ora(`Pushing to ${theme.name} (#${theme.id})…`).start();\n\n const result = await syncer.uploadTheme({\n delete: !opts.nodelete,\n validate: !opts.force,\n onProgress: (d, total) => {\n spinner.text = `Pushing ${d}/${total} files…`;\n },\n });\n\n if (result.validationFailed) {\n spinner.fail(\n `Schema validation failed (${result.errors.length} error(s)). Use --force to skip.`,\n );\n for (const e of result.errors) console.error(` ${e}`);\n process.exit(1);\n } else if (result.errors.length) {\n spinner.warn(`Pushed with ${result.errors.length} error(s).`);\n for (const e of result.errors) console.error(` ${e}`);\n } else {\n spinner.succeed(\n `Pushed ${result.uploaded} file(s), deleted ${result.deleted} remote file(s).`,\n );\n }\n\n // Update stored checksums after successful push\n if (config) {\n writeThemeConfig(themeRoot.root, {\n ...config,\n checksums: syncer.remoteChecksums(),\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","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 { 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\nfunction formatRelativeTime(iso: string): string {\n const diff = Date.now() - new Date(iso).getTime();\n const minutes = Math.floor(diff / 60_000);\n if (minutes < 1) return \"just now\";\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n if (days === 1) return \"yesterday\";\n const date = new Date(iso);\n return `${days}d ago (${date.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\", year: \"numeric\" })})`;\n}\n\n/**\n * Detect files where both local and remote have changed since the last pull.\n * Returns the set of conflicting resource keys.\n */\nfunction detectConflicts(\n storedChecksums: Record<string, string>,\n remoteChecksums: Record<string, string>,\n themeRoot: ThemeRoot,\n): string[] {\n const conflicts: string[] = [];\n for (const [key, storedChecksum] of Object.entries(storedChecksums)) {\n const remoteChecksum = remoteChecksums[key];\n if (remoteChecksum === undefined) continue; // deleted on remote, not a conflict\n if (remoteChecksum === storedChecksum) continue; // remote unchanged\n\n // Remote changed — check if local also changed\n const file = themeRoot.file(key);\n if (!file.exists) continue; // local deleted, not a conflict (remote wins)\n const localChecksum = file.checksum();\n if (localChecksum === storedChecksum) continue; // local unchanged, safe to overwrite\n if (localChecksum === remoteChecksum) continue; // both sides made same change\n\n conflicts.push(key);\n }\n return conflicts;\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 .action(\n async (opts: {\n theme?: string;\n nodelete?: boolean;\n root?: string;\n yes?: 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 // Resolve output directory\n const subdomain = await fetchCompanySubdomain(api);\n let root: string;\n if (opts.root) {\n root = opts.root;\n } else if (workspace) {\n // If already inside local/{company}/, use that directory\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 // Pre-flight summary\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 if (existingConfig?.lastPulledAt) {\n console.log(\n ` Last pulled: ${formatRelativeTime(existingConfig.lastPulledAt)}`,\n );\n }\n console.log();\n\n // Conflict detection — only possible if we have a previous pull's checksums\n const themeRoot = new ThemeRoot(root);\n let skipKeys: Set<string> | undefined;\n\n if (existingConfig?.checksums) {\n const fetchSpinner = ora(\"Checking for conflicts…\").start();\n const syncer = new Syncer(api, theme.id, themeRoot);\n await syncer.fetchChecksums();\n const remoteChecksums = syncer.remoteChecksums();\n const conflicts = detectConflicts(\n existingConfig.checksums,\n remoteChecksums,\n themeRoot,\n );\n fetchSpinner.stop();\n\n if (conflicts.length > 0) {\n console.log(\n chalk.yellow(`⚠ ${conflicts.length} conflict(s) detected:\\n`),\n );\n for (const key of conflicts) {\n console.log(` ${key}`);\n }\n console.log();\n\n const { resolution } = await prompts(\n {\n type: \"select\",\n name: \"resolution\",\n message: \"How do you want to handle conflicts?\",\n choices: [\n {\n title: \"Keep local (skip conflicting files)\",\n value: \"keep-local\",\n },\n {\n title: \"Use remote (overwrite local changes)\",\n value: \"use-remote\",\n },\n { title: \"Abort\", value: \"abort\" },\n ],\n },\n { onCancel: () => process.exit(130) },\n );\n\n if (resolution === \"abort\") {\n console.log(\"Aborted.\");\n process.exit(0);\n }\n\n if (resolution === \"keep-local\") {\n skipKeys = new Set(conflicts);\n }\n // \"use-remote\" → skipKeys stays undefined, everything gets overwritten\n }\n }\n\n if (!opts.yes && !skipKeys) {\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 syncer = new Syncer(api, theme.id, themeRoot);\n const spinner = ora(`Pulling ${theme.name} (#${theme.id})…`).start();\n\n const result = await syncer.downloadTheme({\n delete: !opts.nodelete,\n skip: skipKeys,\n onProgress: (d, total) => {\n spinner.text = `Downloading ${d}/${total} files…`;\n },\n });\n\n // Write .fluid-theme.json with the post-pull state.\n // For skipped files, preserve the old stored checksum so the conflict\n // is still detected on the next pull (instead of silently overwriting).\n const newChecksums = syncer.remoteChecksums();\n if (skipKeys && existingConfig?.checksums) {\n for (const key of skipKeys) {\n const oldChecksum = existingConfig.checksums[key];\n if (oldChecksum) {\n newChecksums[key] = oldChecksum;\n }\n }\n }\n\n writeThemeConfig(absoluteRoot, {\n themeId: theme.id,\n themeName: theme.name,\n company: subdomain,\n lastPulledAt: new Date().toISOString(),\n checksums: newChecksums,\n });\n\n const parts: string[] = [`Downloaded ${result.downloaded} file(s)`];\n if (result.deleted > 0)\n parts.push(`deleted ${result.deleted} local file(s)`);\n if (result.skipped > 0)\n parts.push(`skipped ${result.skipped} conflict(s)`);\n\n if (result.errors.length) {\n spinner.warn(`Pulled with ${result.errors.length} error(s).`);\n for (const e of result.errors) console.error(` ${e}`);\n } else {\n spinner.succeed(`${parts.join(\", \")}.`);\n }\n },\n );\n}\n","import chalk from \"chalk\";\nimport { Command } from \"commander\";\nimport {\n findMissingSectionReferences,\n validateSchemaText,\n VALID_SETTING_TYPES,\n type BlocksSchemaType,\n type Diagnostic,\n type TemplateInput,\n} from \"@fluid-app/theme-schema\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { 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 { 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\";\n\nexport function registerThemeCommand(ctx: PluginContext): void {\n const cmd = new Command(\"theme\").description(\n \"Theme developer workflow — dev server, push, pull, lint, init\",\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\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","../src/theme/dev-server/watcher.ts","../../../api-clients/themes/src/namespaces/v0.ts","../src/theme/syncer.ts","../src/theme/dev-server/index.ts","../src/theme-picker.ts","../src/workspace.ts","../src/commands/dev.ts","../src/commands/push.ts","../src/commands/pull.ts","../src/commands/lint.ts","../src/commands/init.ts","../src/commands/navigate.ts","../src/fs/replace-directory.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 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/** 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","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\";\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\nexport class Syncer {\n private checksums = new Map<string, string>();\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 const body = await themes.listThemeResources(this.api, this.themeId);\n this.updateChecksums(body.application_theme_resources ?? []);\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): Promise<void> {\n if (file.isText) {\n await themes.updateThemeResource(this.api, this.themeId, {\n application_theme_resource: {\n key: file.relativePath,\n content: file.read(),\n },\n });\n } else {\n await this.uploadBinaryFile(file);\n }\n }\n\n private async uploadBinaryFile(file: ThemeFile): 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\n await themes.updateThemeResource(this.api, this.themeId, {\n application_theme_resource: {\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 });\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(relativePath: string): Promise<void> {\n await themes.deleteThemeResource(this.api, this.themeId, {\n application_theme_resource: { key: relativePath },\n });\n this.checksums.delete(relativePath);\n }\n\n // ─── Download ─────────────────────────────────────────────────────────────\n\n async downloadAll(): Promise<RemoteResource[]> {\n const body = await themes.listThemeResources(this.api, this.themeId);\n const resources = body.application_theme_resources ?? [];\n this.updateChecksums(resources);\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 } = {},\n ): Promise<SyncResult> {\n await this.fetchChecksums();\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);\n result.uploaded++;\n } catch (e) {\n result.errors.push(`Upload ${file.relativePath}: ${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);\n result.deleted++;\n } catch (e) {\n result.errors.push(`Delete ${key}: ${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}: ${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)) {\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\n return result;\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\";\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 await new Promise<void>((resolve, reject) => {\n server.listen(opts.port, opts.host, () => resolve());\n server.on(\"error\", reject);\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 { 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 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 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 { 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\";\n\n/**\n * Detect files where the remote has changed since the last pull,\n * and we also have local changes (i.e. we'd overwrite someone else's work).\n */\nfunction detectRemoteDrift(\n storedChecksums: Record<string, string>,\n remoteChecksums: Record<string, string>,\n themeRoot: ThemeRoot,\n): string[] {\n const conflicts: string[] = [];\n for (const [key, storedChecksum] of Object.entries(storedChecksums)) {\n const remoteChecksum = remoteChecksums[key];\n if (remoteChecksum === undefined) continue;\n if (remoteChecksum === storedChecksum) continue; // remote unchanged since pull\n\n // Remote changed — check if we also have this file locally (and it differs)\n const file = themeRoot.file(key);\n if (!file.exists) continue;\n const localChecksum = file.checksum();\n if (localChecksum === remoteChecksum) continue; // local matches remote already\n\n conflicts.push(key);\n }\n return conflicts;\n}\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(\"-f, --force\", \"Skip schema validation\")\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 // 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 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 // Use .fluid-theme.json as the default\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 // Check for remote drift if we have stored checksums\n if (config?.checksums && !opts.force) {\n const driftSpinner = ora(\"Checking for remote changes…\").start();\n const driftSyncer = new Syncer(api, theme.id, themeRoot);\n await driftSyncer.fetchChecksums();\n const remoteChecksums = driftSyncer.remoteChecksums();\n const conflicts = detectRemoteDrift(\n config.checksums,\n remoteChecksums,\n themeRoot,\n );\n driftSpinner.stop();\n\n if (conflicts.length > 0) {\n console.log(\n chalk.yellow(\n `\\n⚠ ${conflicts.length} file(s) changed on remote since last pull:\\n`,\n ),\n );\n for (const key of conflicts) {\n console.log(` ${key}`);\n }\n console.log();\n\n const { resolution } = await prompts(\n {\n type: \"select\",\n name: \"resolution\",\n message: \"How do you want to handle this?\",\n choices: [\n {\n title: \"Push anyway (overwrite remote changes)\",\n value: \"push\",\n },\n {\n title: \"Pull first, then push\",\n value: \"pull-first\",\n },\n { title: \"Abort\", value: \"abort\" },\n ],\n },\n { onCancel: () => process.exit(130) },\n );\n\n if (resolution === \"abort\") {\n console.log(\"Aborted.\");\n process.exit(0);\n }\n if (resolution === \"pull-first\") {\n console.log(\n `Run ${chalk.cyan(\"fluid theme pull\")} first, then push again.`,\n );\n process.exit(0);\n }\n }\n }\n\n const syncer = new Syncer(api, theme.id, themeRoot);\n const spinner = ora(`Pushing to ${theme.name} (#${theme.id})…`).start();\n\n const result = await syncer.uploadTheme({\n delete: !opts.nodelete,\n validate: !opts.force,\n onProgress: (d, total) => {\n spinner.text = `Pushing ${d}/${total} files…`;\n },\n });\n\n if (result.validationFailed) {\n spinner.fail(\n `Schema validation failed (${result.errors.length} error(s)). Use --force to skip.`,\n );\n for (const e of result.errors) console.error(` ${e}`);\n process.exit(1);\n } else if (result.errors.length) {\n spinner.warn(`Pushed with ${result.errors.length} error(s).`);\n for (const e of result.errors) console.error(` ${e}`);\n } else {\n spinner.succeed(\n `Pushed ${result.uploaded} file(s), deleted ${result.deleted} remote file(s).`,\n );\n }\n\n // Update stored checksums after successful push\n if (config) {\n writeThemeConfig(themeRoot.root, {\n ...config,\n checksums: syncer.remoteChecksums(),\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","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 { 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\nfunction formatRelativeTime(iso: string): string {\n const diff = Date.now() - new Date(iso).getTime();\n const minutes = Math.floor(diff / 60_000);\n if (minutes < 1) return \"just now\";\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n if (days === 1) return \"yesterday\";\n const date = new Date(iso);\n return `${days}d ago (${date.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\", year: \"numeric\" })})`;\n}\n\n/**\n * Detect files where both local and remote have changed since the last pull.\n * Returns the set of conflicting resource keys.\n */\nfunction detectConflicts(\n storedChecksums: Record<string, string>,\n remoteChecksums: Record<string, string>,\n themeRoot: ThemeRoot,\n): string[] {\n const conflicts: string[] = [];\n for (const [key, storedChecksum] of Object.entries(storedChecksums)) {\n const remoteChecksum = remoteChecksums[key];\n if (remoteChecksum === undefined) continue; // deleted on remote, not a conflict\n if (remoteChecksum === storedChecksum) continue; // remote unchanged\n\n // Remote changed — check if local also changed\n const file = themeRoot.file(key);\n if (!file.exists) continue; // local deleted, not a conflict (remote wins)\n const localChecksum = file.checksum();\n if (localChecksum === storedChecksum) continue; // local unchanged, safe to overwrite\n if (localChecksum === remoteChecksum) continue; // both sides made same change\n\n conflicts.push(key);\n }\n return conflicts;\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 .action(\n async (opts: {\n theme?: string;\n nodelete?: boolean;\n root?: string;\n yes?: 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 // Resolve output directory\n const subdomain = await fetchCompanySubdomain(api);\n let root: string;\n if (opts.root) {\n root = opts.root;\n } else if (workspace) {\n // If already inside local/{company}/, use that directory\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 // Pre-flight summary\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 if (existingConfig?.lastPulledAt) {\n console.log(\n ` Last pulled: ${formatRelativeTime(existingConfig.lastPulledAt)}`,\n );\n }\n console.log();\n\n // Conflict detection — only possible if we have a previous pull's checksums\n const themeRoot = new ThemeRoot(root);\n let skipKeys: Set<string> | undefined;\n\n if (existingConfig?.checksums) {\n const fetchSpinner = ora(\"Checking for conflicts…\").start();\n const syncer = new Syncer(api, theme.id, themeRoot);\n await syncer.fetchChecksums();\n const remoteChecksums = syncer.remoteChecksums();\n const conflicts = detectConflicts(\n existingConfig.checksums,\n remoteChecksums,\n themeRoot,\n );\n fetchSpinner.stop();\n\n if (conflicts.length > 0) {\n console.log(\n chalk.yellow(`⚠ ${conflicts.length} conflict(s) detected:\\n`),\n );\n for (const key of conflicts) {\n console.log(` ${key}`);\n }\n console.log();\n\n const { resolution } = await prompts(\n {\n type: \"select\",\n name: \"resolution\",\n message: \"How do you want to handle conflicts?\",\n choices: [\n {\n title: \"Keep local (skip conflicting files)\",\n value: \"keep-local\",\n },\n {\n title: \"Use remote (overwrite local changes)\",\n value: \"use-remote\",\n },\n { title: \"Abort\", value: \"abort\" },\n ],\n },\n { onCancel: () => process.exit(130) },\n );\n\n if (resolution === \"abort\") {\n console.log(\"Aborted.\");\n process.exit(0);\n }\n\n if (resolution === \"keep-local\") {\n skipKeys = new Set(conflicts);\n }\n // \"use-remote\" → skipKeys stays undefined, everything gets overwritten\n }\n }\n\n if (!opts.yes && !skipKeys) {\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 syncer = new Syncer(api, theme.id, themeRoot);\n const spinner = ora(`Pulling ${theme.name} (#${theme.id})…`).start();\n\n const result = await syncer.downloadTheme({\n delete: !opts.nodelete,\n skip: skipKeys,\n onProgress: (d, total) => {\n spinner.text = `Downloading ${d}/${total} files…`;\n },\n });\n\n // Write .fluid-theme.json with the post-pull state.\n // For skipped files, preserve the old stored checksum so the conflict\n // is still detected on the next pull (instead of silently overwriting).\n const newChecksums = syncer.remoteChecksums();\n if (skipKeys && existingConfig?.checksums) {\n for (const key of skipKeys) {\n const oldChecksum = existingConfig.checksums[key];\n if (oldChecksum) {\n newChecksums[key] = oldChecksum;\n }\n }\n }\n\n writeThemeConfig(absoluteRoot, {\n themeId: theme.id,\n themeName: theme.name,\n company: subdomain,\n lastPulledAt: new Date().toISOString(),\n checksums: newChecksums,\n });\n\n const parts: string[] = [`Downloaded ${result.downloaded} file(s)`];\n if (result.deleted > 0)\n parts.push(`deleted ${result.deleted} local file(s)`);\n if (result.skipped > 0)\n parts.push(`skipped ${result.skipped} conflict(s)`);\n\n if (result.errors.length) {\n spinner.warn(`Pulled with ${result.errors.length} error(s).`);\n for (const e of result.errors) console.error(` ${e}`);\n } else {\n spinner.succeed(`${parts.join(\", \")}.`);\n }\n },\n );\n}\n","import chalk from \"chalk\";\nimport { Command } from \"commander\";\nimport {\n findMissingSectionReferences,\n validateSchemaText,\n VALID_SETTING_TYPES,\n type BlocksSchemaType,\n type Diagnostic,\n type TemplateInput,\n} from \"@fluid-app/theme-schema\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { 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 { cpSync, existsSync, renameSync, rmSync } from \"node:fs\";\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 */\nexport function replaceDirectory(source: string, target: string): void {\n const staging = reserveSiblingPath(target, \"staging\");\n try {\n cpSync(source, staging, { recursive: true });\n } catch (error) {\n rmSync(staging, { recursive: true, force: true });\n throw error;\n }\n\n // No existing target: a single rename moves the staged copy into place.\n if (!existsSync(target)) {\n swapIntoPlace(staging, target, null);\n return;\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 rmSync(staging, { recursive: true, force: true });\n throw error;\n }\n 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) so the caller is never left without a\n// working directory, then discard the staged copy and rethrow.\nfunction swapIntoPlace(\n staging: string,\n target: string,\n backup: string | null,\n): void {\n try {\n renameSync(staging, target);\n } catch (error) {\n rmSync(staging, { recursive: true, force: true });\n if (backup !== null) restoreBackup(backup, target, error);\n throw error;\n }\n if (backup !== null) rmSync(backup, { recursive: true, force: true });\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// 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 { existsSync, mkdirSync, readdirSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { replaceDirectory } from \"../fs/replace-directory.js\";\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\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 } = 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 replaceDirectory(from, to);\n installed.push(name);\n }\n\n return { installed, skipped };\n}\n","import { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport chalk from \"chalk\";\nimport { Command } from \"commander\";\nimport prompts from \"prompts\";\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\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 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 });\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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"}