@fluid-app/fluid-cli-theme-dev 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @fluid-app/fluid-cli-theme-dev@0.1.2 build /home/runner/_work/fluid-mono/fluid-mono/packages/cli/theme-dev
2
+ > @fluid-app/fluid-cli-theme-dev@0.1.3 build /home/runner/_work/fluid-mono/fluid-mono/packages/cli/theme-dev
3
3
  > tsdown
4
4
 
5
5
  ℹ tsdown v0.21.0 powered by rolldown v1.0.0-rc.7
@@ -8,11 +8,11 @@
8
8
  ℹ target: node18
9
9
  ℹ tsconfig: tsconfig.json
10
10
  ℹ Build start
11
- ℹ dist/index.mjs 38.99 kB │ gzip: 11.20 kB
12
- ℹ dist/index.mjs.map 86.91 kB │ gzip: 21.69 kB
11
+ ℹ dist/index.mjs 39.25 kB │ gzip: 11.30 kB
12
+ ℹ dist/index.mjs.map 87.52 kB │ gzip: 21.90 kB
13
13
  ℹ dist/index.d.mts.map  0.11 kB │ gzip: 0.12 kB
14
14
  ℹ dist/index.d.mts  0.19 kB │ gzip: 0.16 kB
15
- ℹ 4 files, total: 126.20 kB
15
+ ℹ 4 files, total: 127.07 kB
16
16
  [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
17
17
 
18
- ✔ Build complete in 4554ms
18
+ ✔ Build complete in 3302ms
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # @fluid-app/fluid-cli-theme-dev
2
+
3
+ Fluid CLI plugin for theme development. Adds `fluid theme` commands for local dev server, push, pull, and scaffolding.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @fluid-app/fluid-cli @fluid-app/fluid-cli-theme-dev
9
+ ```
10
+
11
+ Requires `@fluid-app/fluid-cli` as the core CLI.
12
+
13
+ ## Authentication
14
+
15
+ Log in before using any theme commands:
16
+
17
+ ```bash
18
+ fluid login
19
+ ```
20
+
21
+ ## Commands
22
+
23
+ ### `fluid theme dev`
24
+
25
+ Start a local dev server that proxies your storefront with hot reload:
26
+
27
+ ```bash
28
+ fluid theme dev
29
+ ```
30
+
31
+ The dev server will:
32
+
33
+ 1. Create (or reuse) a development theme
34
+ 2. Upload all local files to the dev theme
35
+ 3. Watch for file changes and sync them automatically
36
+ 4. Proxy requests to `{company}.fluid.app` with local file overrides
37
+
38
+ | Flag | Default | Description |
39
+ | -------------------------- | ----------- | ----------------------------------------- |
40
+ | `--host <host>` | `127.0.0.1` | Local server host |
41
+ | `--port <port>` | `9292` | Local server port |
42
+ | `-t, --theme <name-or-id>` | auto | Use a specific theme instead of dev theme |
43
+ | `--live-reload <mode>` | `full-page` | Reload mode: `full-page` or `off` |
44
+ | `--navigate` | off | Open browser navigator after start |
45
+ | `--root <path>` | `.` | Theme root directory |
46
+
47
+ ### `fluid theme push`
48
+
49
+ Upload local theme files to a remote theme:
50
+
51
+ ```bash
52
+ fluid theme push # Interactive theme selection
53
+ fluid theme push --theme "My Theme" # By name
54
+ fluid theme push --theme 42 # By ID
55
+ fluid theme push --publish # Push and publish
56
+ fluid theme push --nodelete # Keep remote files not present locally
57
+ ```
58
+
59
+ ### `fluid theme pull`
60
+
61
+ Download a remote theme to your local directory:
62
+
63
+ ```bash
64
+ fluid theme pull # Interactive theme selection
65
+ fluid theme pull --theme "My Theme" # By name or ID
66
+ fluid theme pull --nodelete # Keep local files not present on remote
67
+ ```
68
+
69
+ ### `fluid theme init`
70
+
71
+ Scaffold a new theme from the base template:
72
+
73
+ ```bash
74
+ fluid theme init my-theme
75
+ cd my-theme
76
+ fluid theme dev
77
+ ```
78
+
79
+ ### `fluid theme navigate`
80
+
81
+ Interactively select a route and open it in the browser (requires a running dev server):
82
+
83
+ ```bash
84
+ fluid theme navigate
85
+ ```
86
+
87
+ ## Theme Directory Structure
88
+
89
+ A valid theme directory must contain at least one of: `templates/`, `assets/`, or `config/`.
90
+
91
+ Use a `.fluidignore` file (same syntax as `.gitignore`) to exclude files from syncing.
92
+
93
+ ## Development
94
+
95
+ For contributors working in `fluid-mono`:
96
+
97
+ ```bash
98
+ pnpm --filter @fluid-app/fluid-cli-theme-dev build
99
+ node packages/cli/core/dist/bin/fluid.mjs theme --help
100
+ ```
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import { getAuthToken, readConfig, updateConfig } from "@fluid-app/fluid-cli";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
4
- import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
4
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { createHash } from "node:crypto";
6
6
  import http from "node:http";
7
7
  import https from "node:https";
@@ -377,7 +377,7 @@ var ThemeRoot = class {
377
377
  }
378
378
  file(pathOrFile) {
379
379
  if (pathOrFile instanceof ThemeFile) return pathOrFile;
380
- return new ThemeFile(join(this.root, pathOrFile), this.root);
380
+ return new ThemeFile(isAbsolute(pathOrFile) ? pathOrFile : join(this.root, pathOrFile), this.root);
381
381
  }
382
382
  glob(dir) {
383
383
  const results = [];
@@ -893,8 +893,18 @@ function createDevCommand() {
893
893
  console.error(`'${opts.root}' does not look like a theme directory.`);
894
894
  process.exit(1);
895
895
  }
896
+ const port = Number(opts.port);
897
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
898
+ console.error(`Invalid port: '${opts.port}'. Must be an integer between 1 and 65535.`);
899
+ process.exit(1);
900
+ }
896
901
  const reloadMode = opts.liveReload === "off" ? "off" : "full-page";
897
902
  const api = createApiClient();
903
+ const company = (await api.get("/api/company/v1/companies/me")).data?.company?.subdomain;
904
+ if (!company) {
905
+ console.error("Could not determine company subdomain. Make sure your token is valid.");
906
+ process.exit(1);
907
+ }
898
908
  const theme = await ensureDevTheme(api, opts.theme);
899
909
  let stop;
900
910
  const cleanup = () => {
@@ -903,15 +913,10 @@ function createDevCommand() {
903
913
  };
904
914
  process.on("SIGINT", cleanup);
905
915
  process.on("SIGTERM", cleanup);
906
- const port = Number(opts.port);
907
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
908
- console.error(`Invalid port: '${opts.port}'. Must be an integer between 1 and 65535.`);
909
- process.exit(1);
910
- }
911
916
  stop = await startDevServer(api, {
912
917
  id: theme.id,
913
918
  name: theme.name,
914
- company: theme.company,
919
+ company,
915
920
  editorUrl: theme.editor_url
916
921
  }, themeRoot, {
917
922
  host: opts.host,
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../platform/api-client-core/src/fetch-client.ts","../src/api.ts","../src/plugin-state.ts","../src/theme/mime-type.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","../src/theme/syncer.ts","../src/theme/dev-server/index.ts","../src/commands/dev.ts","../src/commands/push.ts","../src/commands/pull.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\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}\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\n constructor(message: string, status: number, data?: unknown) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.data = data;\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(): { name: string; message: string; status: number; data: unknown } {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n data: this.data,\n };\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 { baseUrl, getAuthToken, onAuthError, defaultHeaders = {} } = config;\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 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 );\n }\n const msg = (data.message || data.error_message) as string | undefined;\n throw new ApiError(\n msg || `${method} request failed`,\n response.status,\n data.errors || data,\n );\n } else {\n throw new ApiError(\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\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 try {\n const data = await response.json();\n return data as TResponse;\n } catch {\n try {\n // API declared JSON content-type but body isn't valid JSON\n const text = await response.text();\n return text as TResponse;\n } catch {\n return null as TResponse;\n }\n }\n }\n\n // Non-JSON response (text/plain, text/html, etc.)\n return null as TResponse;\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 } = 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 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 fetch(url, fetchOptions);\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 { method = \"POST\", headers: customHeaders, signal } = 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 (signal) fetchOptions.signal = signal;\n response = await fetch(url, fetchOptions);\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 { createFetchClient } from \"@fluid-app/api-client-core\";\nimport type { FetchClientInstance } from \"@fluid-app/api-client-core\";\nimport { getAuthToken } from \"@fluid-app/fluid-cli\";\n\nexport type ApiClient = FetchClientInstance;\n\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 { readConfig, updateConfig } from \"@fluid-app/fluid-cli\";\n\ninterface ThemeDevState {\n devThemeId?: number;\n devThemeName?: string;\n}\n\nconst PLUGIN_KEY = \"theme-dev\";\n\nexport function getPluginState(): ThemeDevState {\n const config = readConfig();\n return (config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {};\n}\n\nexport function setPluginState(updates: Partial<ThemeDevState>): void {\n updateConfig((config) => ({\n ...config,\n plugins: {\n ...config.plugins,\n [PLUGIN_KEY]: {\n ...((config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {}),\n ...updates,\n },\n },\n }));\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 {\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\";\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","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 { 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 return new ThemeFile(join(this.root, pathOrFile), 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","import { sep } from \"node:path\";\nimport type { ApiClient } from \"../api.js\";\nimport type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\n\nexport interface RemoteResource {\n key: string;\n checksum: string;\n content?: string;\n url?: string;\n resource_type: string;\n}\n\nexport interface SyncResult {\n uploaded: number;\n downloaded: number;\n deleted: number;\n errors: string[];\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 this.api.get<{\n application_theme_resources: RemoteResource[];\n }>(`/api/application_themes/${this.themeId}/resources`);\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) 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 // ─── Upload ───────────────────────────────────────────────────────────────\n\n async uploadFile(file: ThemeFile): Promise<void> {\n const path = `/api/application_themes/${this.themeId}/resources`;\n if (file.isText) {\n await this.api.put(path, {\n application_theme_resource: {\n key: file.relativePath,\n content: file.read(),\n },\n });\n } else {\n await this.uploadBinaryFile(file, path);\n }\n }\n\n private async uploadBinaryFile(\n file: ThemeFile,\n resourcePath: string,\n ): Promise<void> {\n // Step 1: Create DAM placeholder\n const placeholderBody = await this.api.post<{\n asset: { id: number; canonical_path: string };\n }>(\"/api/dam/assets\", {\n placeholder_asset: {\n description: `Uploaded via Fluid CLI: ${file.name}`,\n mime_type: file.mime.name,\n name: file.name,\n },\n });\n const asset = placeholderBody.asset;\n\n // Step 2: Get ImageKit auth token\n const authBody = await this.api.post<{\n token: string;\n signature: string;\n expire: number;\n }>(\"/api/dam/assets/imagekit_auth\", {});\n\n // Step 3: Upload to ImageKit via multipart\n const folder = this.canonicalPathToImageKitFolder(asset.canonical_path);\n const formData = new FormData();\n const blob = new Blob([file.readBinary() as unknown as ArrayBuffer], {\n type: file.mime.name,\n });\n formData.append(\"file\", blob, file.name);\n formData.append(\"token\", authBody.token);\n formData.append(\"signature\", authBody.signature);\n formData.append(\"expire\", String(authBody.expire));\n formData.append(\"folder\", folder);\n formData.append(\"fileName\", file.name);\n formData.append(\"publicKey\", \"public_j7s4Ih9ETh/OCp41mVQH7tlXBdU=\");\n\n const ikResp = await fetch(\n \"https://upload.imagekit.io/api/v1/files/upload\",\n {\n method: \"POST\",\n body: formData,\n },\n );\n if (!ikResp.ok) throw new Error(`ImageKit upload failed: ${ikResp.status}`);\n const ikBody = (await ikResp.json()) as {\n fileId: string;\n url: string;\n thumbnailUrl: string;\n size: number;\n height?: number;\n width?: number;\n };\n\n // Step 4: Backfill DAM asset\n const backfillPayload: Record<string, unknown> = {\n asset: {\n id: asset.id,\n imagekit_file_id: ikBody.fileId,\n imagekit_url: ikBody.url,\n mime_type: file.mime.name,\n name: file.name,\n file_size: ikBody.size,\n expected_path: asset.canonical_path,\n },\n };\n if (ikBody.height)\n (backfillPayload[\"asset\"] as Record<string, unknown>)[\"height\"] =\n ikBody.height;\n if (ikBody.width)\n (backfillPayload[\"asset\"] as Record<string, unknown>)[\"width\"] =\n ikBody.width;\n\n const backfillBody = await this.api.post<{\n asset: { code: string; default_variant_url: string };\n }>(\"/api/dam/assets/backfill_imagekit\", backfillPayload);\n\n // Step 5: Associate with theme resource\n await this.api.put(resourcePath, {\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 this.api.delete(`/api/application_themes/${this.themeId}/resources`, {\n body: { 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 this.api.get<{\n application_theme_resources: RemoteResource[];\n }>(`/api/application_themes/${this.themeId}/resources`);\n this.updateChecksums(body.application_theme_resources ?? []);\n return body.application_theme_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 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 };\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 onProgress?: (done: number, total: number) => void;\n } = {},\n ): Promise<SyncResult> {\n const resources = await this.downloadAll();\n const result: SyncResult = {\n uploaded: 0,\n deleted: 0,\n downloaded: 0,\n errors: [],\n };\n\n let done = 0;\n for (const resource of resources) {\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 (resource.content !== undefined) {\n file.write(resource.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,\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 await syncer.uploadTheme({\n delete: true,\n onProgress: (done, total) => {\n process.stdout.write(`\\r Uploading ${done}/${total} files…`);\n },\n });\n process.stdout.write(\"\\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 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 { Command } from \"commander\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { getPluginState, setPluginState } from \"../plugin-state.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { startDevServer } from \"../theme/dev-server/index.js\";\n\ninterface ApplicationTheme {\n id: number;\n name: string;\n company: string;\n editor_url?: string;\n}\n\nasync function ensureDevTheme(\n api: ReturnType<typeof createApiClient>,\n identifier?: string,\n): Promise<ApplicationTheme> {\n if (identifier) {\n const body = await api.get<{ application_themes: ApplicationTheme[] }>(\n \"/api/application_themes\",\n );\n const found =\n (body.application_themes ?? []).find(\n (t) => String(t.id) === identifier,\n ) ??\n (body.application_themes ?? []).find(\n (t) => t.name.toLowerCase() === identifier.toLowerCase(),\n );\n if (!found) {\n console.error(`Theme not found: ${identifier}`);\n process.exit(1);\n }\n return found;\n }\n\n // Reuse stored dev theme if it still exists\n const { devThemeId } = getPluginState();\n if (devThemeId) {\n try {\n const body = await api.get<{ application_theme: ApplicationTheme }>(\n `/api/application_themes/${devThemeId}`,\n );\n if (body.application_theme) {\n console.log(`Using existing dev theme #${devThemeId}`);\n return body.application_theme;\n }\n } catch {\n // Theme no longer exists — create a new one\n }\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 api.post<{ application_theme: ApplicationTheme }>(\n \"/api/application_themes\",\n { application_theme: { name, role: \"development\" } },\n );\n\n const theme = body.application_theme;\n setPluginState({ devThemeId: theme.id, devThemeName: 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 const themeRoot = new ThemeRoot(opts.root);\n if (!themeRoot.isValid()) {\n console.error(`'${opts.root}' does not look like a theme directory.`);\n process.exit(1);\n }\n\n const reloadMode = opts.liveReload === \"off\" ? \"off\" : \"full-page\";\n const api = createApiClient();\n const theme = await ensureDevTheme(api, opts.theme);\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 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 stop = await startDevServer(\n api,\n {\n id: theme.id,\n name: theme.name,\n company: theme.company,\n editorUrl: theme.editor_url,\n },\n themeRoot,\n { host: opts.host, port, reloadMode },\n (address) => {\n console.log(`\\n Dev server: ${address}`);\n if (theme.editor_url)\n console.log(` Web editor: ${theme.editor_url}`);\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 { Command } from \"commander\";\nimport ora from \"ora\";\nimport prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { Syncer } from \"../theme/syncer.js\";\n\ninterface ApplicationTheme {\n id: number;\n name: string;\n company: string;\n}\n\nasync function selectTheme(\n api: ReturnType<typeof createApiClient>,\n): Promise<ApplicationTheme> {\n const body = await api.get<{ application_themes: ApplicationTheme[] }>(\n \"/api/application_themes\",\n );\n const themes = body.application_themes ?? [];\n if (!themes.length) {\n console.error(\"No themes found.\");\n process.exit(1);\n }\n const { id } = await prompts(\n {\n type: \"select\",\n name: \"id\",\n message: \"Select a theme to push to\",\n choices: themes.map((t) => ({\n title: `${t.name} (#${t.id})`,\n value: t.id,\n })),\n },\n { onCancel: () => process.exit(130) },\n );\n if (!id) {\n console.error(\"No theme selected.\");\n process.exit(1);\n }\n return themes.find((t) => t.id === id)!;\n}\n\nasync function findTheme(\n api: ReturnType<typeof createApiClient>,\n identifier: string,\n): Promise<ApplicationTheme> {\n const body = await api.get<{ application_themes: ApplicationTheme[] }>(\n \"/api/application_themes\",\n );\n const themes = body.application_themes ?? [];\n const found =\n themes.find((t) => String(t.id) === identifier) ??\n themes.find((t) => t.name.toLowerCase() === identifier.toLowerCase());\n if (!found) {\n console.error(`No theme found with identifier: ${identifier}`);\n process.exit(1);\n }\n return found;\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(\"--root <path>\", \"Theme root directory\", \".\")\n .action(\n async (opts: {\n theme?: string;\n nodelete?: boolean;\n force?: boolean;\n publish?: boolean;\n root: string;\n }) => {\n requireToken();\n\n const themeRoot = new ThemeRoot(opts.root);\n if (!themeRoot.isValid()) {\n console.error(`'${opts.root}' does not look like a theme directory.`);\n process.exit(1);\n }\n\n const api = createApiClient();\n const theme = opts.theme\n ? await findTheme(api, opts.theme)\n : await selectTheme(api);\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 onProgress: (d, total) => {\n spinner.text = `Pushing ${d}/${total} files…`;\n },\n });\n\n 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 if (opts.publish) {\n const pubSpinner = ora(\"Publishing theme…\").start();\n try {\n await api.post(`/api/application_themes/${theme.id}/publish`);\n pubSpinner.succeed(\"Theme published.\");\n } catch (e) {\n pubSpinner.fail(`Publish failed: ${e}`);\n }\n }\n },\n );\n}\n","import { Command } from \"commander\";\nimport ora from \"ora\";\nimport prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { Syncer } from \"../theme/syncer.js\";\n\ninterface ApplicationTheme {\n id: number;\n name: string;\n}\n\nasync function selectOrFindTheme(\n api: ReturnType<typeof createApiClient>,\n identifier?: string,\n): Promise<ApplicationTheme> {\n const body = await api.get<{ application_themes: ApplicationTheme[] }>(\n \"/api/application_themes\",\n );\n const themes = body.application_themes ?? [];\n if (!themes.length) {\n console.error(\"No themes found.\");\n process.exit(1);\n }\n\n if (identifier) {\n const found =\n themes.find((t) => String(t.id) === identifier) ??\n themes.find((t) => t.name.toLowerCase() === identifier.toLowerCase());\n if (!found) {\n console.error(`No theme found with identifier: ${identifier}`);\n process.exit(1);\n }\n return found;\n }\n\n const { id } = await prompts(\n {\n type: \"select\",\n name: \"id\",\n message: \"Select a theme to pull\",\n choices: themes.map((t) => ({\n title: `${t.name} (#${t.id})`,\n value: t.id,\n })),\n },\n { onCancel: () => process.exit(130) },\n );\n if (!id) {\n console.error(\"No theme selected.\");\n process.exit(1);\n }\n return themes.find((t) => t.id === id)!;\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 .action(\n async (opts: { theme?: string; nodelete?: boolean; root: string }) => {\n requireToken();\n\n const api = createApiClient();\n const theme = await selectOrFindTheme(api, opts.theme);\n const themeRoot = new ThemeRoot(opts.root);\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 onProgress: (d, total) => {\n spinner.text = `Downloading ${d}/${total} files…`;\n },\n });\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(\n `Downloaded ${result.downloaded} file(s), deleted ${result.deleted} local file(s).`,\n );\n }\n },\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 { getPluginState } from \"../plugin-state.js\";\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] as const;\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\n ? Number(opts.theme)\n : getPluginState().devThemeId;\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: \"select\",\n name: \"dest\",\n message: \"Select a route\",\n choices,\n },\n { onCancel },\n );\n\n if (!dest) return;\n\n let path: string;\n if (typeof dest === \"string\") {\n path = dest;\n } else {\n const api = createApiClient();\n const body = await api.get<{\n available_themeables: Array<{ slug: string; title?: string }>;\n }>(`/api/application_themes/${themeId}/available_themeables`, {\n themeable: dest.resourceType,\n 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 { slug } = await prompts(\n {\n type: \"select\",\n name: \"slug\",\n message: `Select a ${dest.label.toLowerCase()}`,\n choices: resources.map((r) => ({\n title: r.title ?? r.slug,\n value: r.slug,\n })),\n },\n { onCancel },\n );\n path = dest.template.replace(\"%s\", slug as string);\n }\n }\n\n const url = `${address}${path}`;\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 { 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, init\",\n );\n\n cmd.addCommand(createDevCommand());\n cmd.addCommand(createPushCommand());\n cmd.addCommand(createPullCommand());\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":[],"sources":["../../../platform/api-client-core/src/fetch-client.ts","../src/api.ts","../src/plugin-state.ts","../src/theme/mime-type.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","../src/theme/syncer.ts","../src/theme/dev-server/index.ts","../src/commands/dev.ts","../src/commands/push.ts","../src/commands/pull.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\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}\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\n constructor(message: string, status: number, data?: unknown) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.data = data;\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(): { name: string; message: string; status: number; data: unknown } {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n data: this.data,\n };\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 { baseUrl, getAuthToken, onAuthError, defaultHeaders = {} } = config;\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 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 );\n }\n const msg = (data.message || data.error_message) as string | undefined;\n throw new ApiError(\n msg || `${method} request failed`,\n response.status,\n data.errors || data,\n );\n } else {\n throw new ApiError(\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\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 try {\n const data = await response.json();\n return data as TResponse;\n } catch {\n try {\n // API declared JSON content-type but body isn't valid JSON\n const text = await response.text();\n return text as TResponse;\n } catch {\n return null as TResponse;\n }\n }\n }\n\n // Non-JSON response (text/plain, text/html, etc.)\n return null as TResponse;\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 } = 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 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 fetch(url, fetchOptions);\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 { method = \"POST\", headers: customHeaders, signal } = 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 (signal) fetchOptions.signal = signal;\n response = await fetch(url, fetchOptions);\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 { createFetchClient } from \"@fluid-app/api-client-core\";\nimport type { FetchClientInstance } from \"@fluid-app/api-client-core\";\nimport { getAuthToken } from \"@fluid-app/fluid-cli\";\n\nexport type ApiClient = FetchClientInstance;\n\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 { readConfig, updateConfig } from \"@fluid-app/fluid-cli\";\n\ninterface ThemeDevState {\n devThemeId?: number;\n devThemeName?: string;\n}\n\nconst PLUGIN_KEY = \"theme-dev\";\n\nexport function getPluginState(): ThemeDevState {\n const config = readConfig();\n return (config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {};\n}\n\nexport function setPluginState(updates: Partial<ThemeDevState>): void {\n updateConfig((config) => ({\n ...config,\n plugins: {\n ...config.plugins,\n [PLUGIN_KEY]: {\n ...((config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {}),\n ...updates,\n },\n },\n }));\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 {\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\";\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","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","import { sep } from \"node:path\";\nimport type { ApiClient } from \"../api.js\";\nimport type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\n\nexport interface RemoteResource {\n key: string;\n checksum: string;\n content?: string;\n url?: string;\n resource_type: string;\n}\n\nexport interface SyncResult {\n uploaded: number;\n downloaded: number;\n deleted: number;\n errors: string[];\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 this.api.get<{\n application_theme_resources: RemoteResource[];\n }>(`/api/application_themes/${this.themeId}/resources`);\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) 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 // ─── Upload ───────────────────────────────────────────────────────────────\n\n async uploadFile(file: ThemeFile): Promise<void> {\n const path = `/api/application_themes/${this.themeId}/resources`;\n if (file.isText) {\n await this.api.put(path, {\n application_theme_resource: {\n key: file.relativePath,\n content: file.read(),\n },\n });\n } else {\n await this.uploadBinaryFile(file, path);\n }\n }\n\n private async uploadBinaryFile(\n file: ThemeFile,\n resourcePath: string,\n ): Promise<void> {\n // Step 1: Create DAM placeholder\n const placeholderBody = await this.api.post<{\n asset: { id: number; canonical_path: string };\n }>(\"/api/dam/assets\", {\n placeholder_asset: {\n description: `Uploaded via Fluid CLI: ${file.name}`,\n mime_type: file.mime.name,\n name: file.name,\n },\n });\n const asset = placeholderBody.asset;\n\n // Step 2: Get ImageKit auth token\n const authBody = await this.api.post<{\n token: string;\n signature: string;\n expire: number;\n }>(\"/api/dam/assets/imagekit_auth\", {});\n\n // Step 3: Upload to ImageKit via multipart\n const folder = this.canonicalPathToImageKitFolder(asset.canonical_path);\n const formData = new FormData();\n const blob = new Blob([file.readBinary() as unknown as ArrayBuffer], {\n type: file.mime.name,\n });\n formData.append(\"file\", blob, file.name);\n formData.append(\"token\", authBody.token);\n formData.append(\"signature\", authBody.signature);\n formData.append(\"expire\", String(authBody.expire));\n formData.append(\"folder\", folder);\n formData.append(\"fileName\", file.name);\n formData.append(\"publicKey\", \"public_j7s4Ih9ETh/OCp41mVQH7tlXBdU=\");\n\n const ikResp = await fetch(\n \"https://upload.imagekit.io/api/v1/files/upload\",\n {\n method: \"POST\",\n body: formData,\n },\n );\n if (!ikResp.ok) throw new Error(`ImageKit upload failed: ${ikResp.status}`);\n const ikBody = (await ikResp.json()) as {\n fileId: string;\n url: string;\n thumbnailUrl: string;\n size: number;\n height?: number;\n width?: number;\n };\n\n // Step 4: Backfill DAM asset\n const backfillPayload: Record<string, unknown> = {\n asset: {\n id: asset.id,\n imagekit_file_id: ikBody.fileId,\n imagekit_url: ikBody.url,\n mime_type: file.mime.name,\n name: file.name,\n file_size: ikBody.size,\n expected_path: asset.canonical_path,\n },\n };\n if (ikBody.height)\n (backfillPayload[\"asset\"] as Record<string, unknown>)[\"height\"] =\n ikBody.height;\n if (ikBody.width)\n (backfillPayload[\"asset\"] as Record<string, unknown>)[\"width\"] =\n ikBody.width;\n\n const backfillBody = await this.api.post<{\n asset: { code: string; default_variant_url: string };\n }>(\"/api/dam/assets/backfill_imagekit\", backfillPayload);\n\n // Step 5: Associate with theme resource\n await this.api.put(resourcePath, {\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 this.api.delete(`/api/application_themes/${this.themeId}/resources`, {\n body: { 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 this.api.get<{\n application_theme_resources: RemoteResource[];\n }>(`/api/application_themes/${this.themeId}/resources`);\n this.updateChecksums(body.application_theme_resources ?? []);\n return body.application_theme_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 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 };\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 onProgress?: (done: number, total: number) => void;\n } = {},\n ): Promise<SyncResult> {\n const resources = await this.downloadAll();\n const result: SyncResult = {\n uploaded: 0,\n deleted: 0,\n downloaded: 0,\n errors: [],\n };\n\n let done = 0;\n for (const resource of resources) {\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 (resource.content !== undefined) {\n file.write(resource.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,\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 await syncer.uploadTheme({\n delete: true,\n onProgress: (done, total) => {\n process.stdout.write(`\\r Uploading ${done}/${total} files…`);\n },\n });\n process.stdout.write(\"\\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 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 { Command } from \"commander\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { getPluginState, setPluginState } from \"../plugin-state.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { startDevServer } from \"../theme/dev-server/index.js\";\n\ninterface ApplicationTheme {\n id: number;\n name: string;\n editor_url?: string;\n}\n\ninterface CompanyMe {\n data: { company: { subdomain?: string; name?: string } };\n}\n\nasync function ensureDevTheme(\n api: ReturnType<typeof createApiClient>,\n identifier?: string,\n): Promise<ApplicationTheme> {\n if (identifier) {\n const body = await api.get<{ application_themes: ApplicationTheme[] }>(\n \"/api/application_themes\",\n );\n const found =\n (body.application_themes ?? []).find(\n (t) => String(t.id) === identifier,\n ) ??\n (body.application_themes ?? []).find(\n (t) => t.name.toLowerCase() === identifier.toLowerCase(),\n );\n if (!found) {\n console.error(`Theme not found: ${identifier}`);\n process.exit(1);\n }\n return found;\n }\n\n // Reuse stored dev theme if it still exists\n const { devThemeId } = getPluginState();\n if (devThemeId) {\n try {\n const body = await api.get<{ application_theme: ApplicationTheme }>(\n `/api/application_themes/${devThemeId}`,\n );\n if (body.application_theme) {\n console.log(`Using existing dev theme #${devThemeId}`);\n return body.application_theme;\n }\n } catch {\n // Theme no longer exists — create a new one\n }\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 api.post<{ application_theme: ApplicationTheme }>(\n \"/api/application_themes\",\n { application_theme: { name, role: \"development\" } },\n );\n const theme = body.application_theme;\n setPluginState({ devThemeId: theme.id, devThemeName: 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 const themeRoot = new ThemeRoot(opts.root);\n if (!themeRoot.isValid()) {\n console.error(`'${opts.root}' 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\n const companyRes = await api.get<CompanyMe>(\n \"/api/company/v1/companies/me\",\n );\n const 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 const theme = await ensureDevTheme(api, opts.theme);\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: theme.editor_url,\n },\n themeRoot,\n { host: opts.host, port, reloadMode },\n (address) => {\n console.log(`\\n Dev server: ${address}`);\n if (theme.editor_url)\n console.log(` Web editor: ${theme.editor_url}`);\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 { Command } from \"commander\";\nimport ora from \"ora\";\nimport prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { Syncer } from \"../theme/syncer.js\";\n\ninterface ApplicationTheme {\n id: number;\n name: string;\n}\n\nasync function selectTheme(\n api: ReturnType<typeof createApiClient>,\n): Promise<ApplicationTheme> {\n const body = await api.get<{ application_themes: ApplicationTheme[] }>(\n \"/api/application_themes\",\n );\n const themes = body.application_themes ?? [];\n if (!themes.length) {\n console.error(\"No themes found.\");\n process.exit(1);\n }\n const { id } = await prompts(\n {\n type: \"select\",\n name: \"id\",\n message: \"Select a theme to push to\",\n choices: themes.map((t) => ({\n title: `${t.name} (#${t.id})`,\n value: t.id,\n })),\n },\n { onCancel: () => process.exit(130) },\n );\n if (!id) {\n console.error(\"No theme selected.\");\n process.exit(1);\n }\n return themes.find((t) => t.id === id)!;\n}\n\nasync function findTheme(\n api: ReturnType<typeof createApiClient>,\n identifier: string,\n): Promise<ApplicationTheme> {\n const body = await api.get<{ application_themes: ApplicationTheme[] }>(\n \"/api/application_themes\",\n );\n const themes = body.application_themes ?? [];\n const found =\n themes.find((t) => String(t.id) === identifier) ??\n themes.find((t) => t.name.toLowerCase() === identifier.toLowerCase());\n if (!found) {\n console.error(`No theme found with identifier: ${identifier}`);\n process.exit(1);\n }\n return found;\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(\"--root <path>\", \"Theme root directory\", \".\")\n .action(\n async (opts: {\n theme?: string;\n nodelete?: boolean;\n force?: boolean;\n publish?: boolean;\n root: string;\n }) => {\n requireToken();\n\n const themeRoot = new ThemeRoot(opts.root);\n if (!themeRoot.isValid()) {\n console.error(`'${opts.root}' does not look like a theme directory.`);\n process.exit(1);\n }\n\n const api = createApiClient();\n const theme = opts.theme\n ? await findTheme(api, opts.theme)\n : await selectTheme(api);\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 onProgress: (d, total) => {\n spinner.text = `Pushing ${d}/${total} files…`;\n },\n });\n\n 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 if (opts.publish) {\n const pubSpinner = ora(\"Publishing theme…\").start();\n try {\n await api.post(`/api/application_themes/${theme.id}/publish`);\n pubSpinner.succeed(\"Theme published.\");\n } catch (e) {\n pubSpinner.fail(`Publish failed: ${e}`);\n }\n }\n },\n );\n}\n","import { Command } from \"commander\";\nimport ora from \"ora\";\nimport prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { Syncer } from \"../theme/syncer.js\";\n\ninterface ApplicationTheme {\n id: number;\n name: string;\n}\n\nasync function selectOrFindTheme(\n api: ReturnType<typeof createApiClient>,\n identifier?: string,\n): Promise<ApplicationTheme> {\n const body = await api.get<{ application_themes: ApplicationTheme[] }>(\n \"/api/application_themes\",\n );\n const themes = body.application_themes ?? [];\n if (!themes.length) {\n console.error(\"No themes found.\");\n process.exit(1);\n }\n\n if (identifier) {\n const found =\n themes.find((t) => String(t.id) === identifier) ??\n themes.find((t) => t.name.toLowerCase() === identifier.toLowerCase());\n if (!found) {\n console.error(`No theme found with identifier: ${identifier}`);\n process.exit(1);\n }\n return found;\n }\n\n const { id } = await prompts(\n {\n type: \"select\",\n name: \"id\",\n message: \"Select a theme to pull\",\n choices: themes.map((t) => ({\n title: `${t.name} (#${t.id})`,\n value: t.id,\n })),\n },\n { onCancel: () => process.exit(130) },\n );\n if (!id) {\n console.error(\"No theme selected.\");\n process.exit(1);\n }\n return themes.find((t) => t.id === id)!;\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 .action(\n async (opts: { theme?: string; nodelete?: boolean; root: string }) => {\n requireToken();\n\n const api = createApiClient();\n const theme = await selectOrFindTheme(api, opts.theme);\n const themeRoot = new ThemeRoot(opts.root);\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 onProgress: (d, total) => {\n spinner.text = `Downloading ${d}/${total} files…`;\n },\n });\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(\n `Downloaded ${result.downloaded} file(s), deleted ${result.deleted} local file(s).`,\n );\n }\n },\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 { getPluginState } from \"../plugin-state.js\";\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] as const;\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\n ? Number(opts.theme)\n : getPluginState().devThemeId;\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: \"select\",\n name: \"dest\",\n message: \"Select a route\",\n choices,\n },\n { onCancel },\n );\n\n if (!dest) return;\n\n let path: string;\n if (typeof dest === \"string\") {\n path = dest;\n } else {\n const api = createApiClient();\n const body = await api.get<{\n available_themeables: Array<{ slug: string; title?: string }>;\n }>(`/api/application_themes/${themeId}/available_themeables`, {\n themeable: dest.resourceType,\n 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 { slug } = await prompts(\n {\n type: \"select\",\n name: \"slug\",\n message: `Select a ${dest.label.toLowerCase()}`,\n choices: resources.map((r) => ({\n title: r.title ?? r.slug,\n value: r.slug,\n })),\n },\n { onCancel },\n );\n path = dest.template.replace(\"%s\", slug as string);\n }\n }\n\n const url = `${address}${path}`;\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 { 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, init\",\n );\n\n cmd.addCommand(createDevCommand());\n cmd.addCommand(createPushCommand());\n cmd.addCommand(createPullCommand());\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":";;;;;;;;;;;;;;;AAwCA,IAAa,WAAb,MAAa,iBAAiB,MAAM;CAClC;CACA;CAEA,YAAY,SAAiB,QAAgB,MAAgB;AAC3D,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,SAAS;AACd,OAAK,OAAO;AAEZ,MAAI,uBAAuB,MAEvB,OAMA,kBAAkB,MAAM,SAAS;;CAIvC,SAA2E;AACzE,SAAO;GACL,MAAM,KAAK;GACX,SAAS,KAAK;GACd,QAAQ,KAAK;GACb,MAAM,KAAK;GACZ;;;;;;AAoDL,SAAgB,kBACd,QACqB;CACrB,MAAM,EAAE,SAAS,cAAc,aAAa,iBAAiB,EAAE,KAAK;;;;CAKpE,eAAe,aACb,eACiC;EACjC,MAAM,UAAkC;GACtC,QAAQ;GACR,gBAAgB;GAChB,GAAG;GACH,GAAG;GACJ;AAGD,MAAI,cAAc;GAChB,MAAM,QAAQ,MAAM,cAAc;AAClC,OAAI,MACF,SAAQ,gBAAgB,UAAU;;AAItC,SAAO;;;;;;;CAQT,SAAS,QAAQ,UAA0B;AACzC,SAAO,GAAG,UAAU;;;;;;CAOtB,SAAS,SACP,UACA,QACQ;EACR,MAAM,UAAU,QAAQ,SAAS;AAEjC,MAAI,CAAC,UAAU,OAAO,KAAK,OAAO,CAAC,WAAW,EAC5C,QAAO;EAGT,MAAM,cAAc,IAAI,iBAAiB;AAEzC,SAAO,QAAQ,OAAO,CAAC,SAAS,CAAC,KAAK,WAAW;AAC/C,OAAI,UAAU,KAAA,KAAa,UAAU,KACnC;AAGF,OAAI,MAAM,QAAQ,MAAM,CAEtB,OAAM,SAAS,SAAS,YAAY,OAAO,GAAG,IAAI,KAAK,OAAO,KAAK,CAAC,CAAC;YAC5D,OAAO,UAAU,SAE1B,QAAO,QAAQ,MAAM,CAAC,SAAS,CAAC,QAAQ,cAAc;AACpD,QAAI,aAAa,KAAA,KAAa,aAAa,KACzC;AAGF,QAAI,MAAM,QAAQ,SAAS,CACzB,UAAS,SAAS,SAChB,YAAY,OAAO,GAAG,IAAI,GAAG,OAAO,MAAM,OAAO,KAAK,CAAC,CACxD;QAED,aAAY,OAAO,GAAG,IAAI,GAAG,OAAO,IAAI,OAAO,SAAS,CAAC;KAE3D;OAEF,aAAY,OAAO,KAAK,OAAO,MAAM,CAAC;IAExC;EAEF,MAAM,KAAK,YAAY,UAAU;AACjC,SAAO,KAAK,GAAG,QAAQ,GAAG,OAAO;;;;;;CAOnC,eAAe,eACb,UACA,QACA,MACoB;AACpB,MAAI,SAAS,WAAW,OAAO,YAC7B,cAAa;AAGf,MAAI,CAAC,SAAS,IAAI;GAGhB,MAAM,YAAY,MAAM,SAAS,MAAM,CAAC,YAAY,GAAG;AAGvD,OAFoB,SAAS,QAAQ,IAAI,eAAe,EAEvC,SAAS,mBAAmB,EAAE;IAC7C,IAAI;AACJ,QAAI;AACF,YAAO,KAAK,MAAM,UAAU;YACtB;AACN,WAAM,IAAI,SACR,UAAU,MAAM,GAAG,IAAI,IACrB,GAAG,OAAO,8BAA8B,SAAS,UACnD,SAAS,QACT,KACD;;AAGH,UAAM,IAAI,SADG,KAAK,WAAW,KAAK,iBAEzB,GAAG,OAAO,kBACjB,SAAS,QACT,KAAK,UAAU,KAChB;SAED,OAAM,IAAI,SACR,GAAG,OAAO,8BAA8B,SAAS,UACjD,SAAS,QACT,KACD;;AAIL,MACE,SAAS,WAAW,OACpB,SAAS,QAAQ,IAAI,iBAAiB,KAAK,IAE3C,QAAO;AAKT,MAFoB,SAAS,QAAQ,IAAI,eAAe,EAEvC,SAAS,mBAAmB,CAC3C,KAAI;AAEF,UADa,MAAM,SAAS,MAAM;UAE5B;AACN,OAAI;AAGF,WADa,MAAM,SAAS,MAAM;WAE5B;AACN,WAAO;;;AAMb,SAAO;;;;;CAMT,eAAe,QACb,UACA,UAA0B,EAAE,EACR;EACpB,MAAM,EACJ,SAAS,OACT,SAAS,eACT,QACA,MACA,WACE;EAEJ,MAAM,MAAM,SAAS,SAAS,UAAU,OAAO,GAAG,QAAQ,SAAS;EAEnE,MAAM,UAAU,MAAM,aAAa,cAAc;EAEjD,IAAI;AAEJ,MAAI;GACF,MAAM,eAA4B;IAAE;IAAQ;IAAS;GACrD,MAAM,iBACJ,QAAQ,WAAW,QAAQ,KAAK,UAAU,KAAK,GAAG;AACpD,OAAI,eAAgB,cAAa,OAAO;AACxC,OAAI,OAAQ,cAAa,SAAS;AAClC,cAAW,MAAM,MAAM,KAAK,aAAa;WAClC,cAAc;AACrB,SAAM,IAAI,SACR,kBAAkB,wBAAwB,QAAQ,aAAa,UAAU,2BACzE,GACA,KACD;;AAGH,SAAO,eAA0B,UAAU,QAAQ,IAAI;;;;;CAMzD,eAAe,oBACb,UACA,UACA,UAEI,EAAE,EACc;EACpB,MAAM,EAAE,SAAS,QAAQ,SAAS,eAAe,WAAW;EAE5D,MAAM,MAAM,QAAQ,SAAS;EAC7B,MAAM,UAAU,MAAM,aAAa,cAAc;AAGjD,SAAO,QAAQ;EAEf,IAAI;AAEJ,MAAI;GACF,MAAM,eAA4B;IAAE;IAAQ;IAAS,MAAM;IAAU;AACrE,OAAI,OAAQ,cAAa,SAAS;AAClC,cAAW,MAAM,MAAM,KAAK,aAAa;WAClC,cAAc;AACrB,SAAM,IAAI,SACR,kBAAkB,wBAAwB,QAAQ,aAAa,UAAU,2BACzE,GACA,KACD;;AAGH,SAAO,eAA0B,UAAU,QAAQ,IAAI;;AAIzD,QAAO;EACI;EACY;EAGrB,MACE,UACA,QACA,YAEA,QAAmB,UAAU;GAC3B,GAAG;GACH,QAAQ;GACR,GAAI,UAAU,EAAE,QAAQ;GACzB,CAAC;EAEJ,OACE,UACA,MACA,YAEA,QAAmB,UAAU;GAC3B,GAAG;GACH,QAAQ;GACR;GACD,CAAC;EAEJ,MACE,UACA,MACA,YAEA,QAAmB,UAAU;GAC3B,GAAG;GACH,QAAQ;GACR;GACD,CAAC;EAEJ,QACE,UACA,MACA,YAEA,QAAmB,UAAU;GAC3B,GAAG;GACH,QAAQ;GACR;GACD,CAAC;EAEJ,SACE,UACA,YAEA,QAAmB,UAAU;GAC3B,GAAG;GACH,QAAQ;GACT,CAAC;EACL;;;;ACtZH,SAAS,aAAqB;AAC5B,QAAO,QAAQ,IAAI,qBAAqB;;AAG1C,SAAgB,gBAAgB,eAAmC;AACjE,QAAO,kBAAkB;EACvB,SAAS,YAAY;EACrB,oBAAoB,iBAAiB,cAAc,IAAI;EACxD,CAAC;;AAGJ,SAAgB,eAAuB;CACrC,MAAM,QAAQ,cAAc;AAC5B,KAAI,CAAC,OAAO;AACV,UAAQ,MAAM,0CAA0C;AACxD,UAAQ,KAAK,EAAE;;AAEjB,QAAO;;;;AChBT,MAAM,aAAa;AAEnB,SAAgB,iBAAgC;AAE9C,QADe,YAAY,CACZ,QAAQ,eAAiC,EAAE;;AAG5D,SAAgB,eAAe,SAAuC;AACpE,eAAc,YAAY;EACxB,GAAG;EACH,SAAS;GACP,GAAG,OAAO;IACT,aAAa;IACZ,GAAK,OAAO,QAAQ,eAAiC,EAAE;IACvD,GAAG;IACJ;GACF;EACF,EAAE;;;;ACxBL,MAAM,aAAqC;CACzC,WAAW;CACX,SAAS;CACT,QAAQ;CACR,OAAO;CACP,SAAS;CACT,QAAQ;CACR,OAAO;CACP,QAAQ;CACT;AAED,MAAM,eAAuC;CAC3C,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,SAAS;CACT,UAAU;CACV,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,QAAQ;CACT;AAOD,SAAgB,YAAY,KAAuB;CACjD,MAAM,OAAO,WAAW;AACxB,KAAI,KAAM,QAAO;EAAE,MAAM;EAAM,QAAQ;EAAM;CAE7C,MAAM,SAAS,aAAa;AAC5B,KAAI,OAAQ,QAAO;EAAE,MAAM;EAAQ,QAAQ;EAAO;AAElD,QAAO;EAAE,MAAM;EAA4B,QAAQ;EAAO;;;;AChC5D,IAAa,YAAb,MAAuB;CACrB;CACA;CACA;CAEA,YAAY,cAAsB,MAAc;AAC9C,OAAK,eAAe;AACpB,OAAK,eAAe,SAAS,MAAM,aAAa;AAChD,OAAK,OAAO,YAAY,QAAQ,aAAa,CAAC,aAAa,CAAC;;CAG9D,IAAI,OAAe;AACjB,SAAO,SAAS,KAAK,aAAa;;CAGpC,IAAI,SAAkB;AACpB,SAAO,KAAK,KAAK;;CAGnB,IAAI,WAAoB;AACtB,SAAO,KAAK,aAAa,SAAS,UAAU;;CAG9C,IAAI,SAAkB;AACpB,SAAO,KAAK,aAAa,SAAS,QAAQ;;CAG5C,IAAI,SAAkB;AACpB,SAAO,WAAW,KAAK,aAAa;;CAGtC,OAAe;AACb,SAAO,aAAa,KAAK,cAAc,QAAQ;;CAGjD,aAAqB;AACnB,SAAO,aAAa,KAAK,aAAa;;CAGxC,MAAM,SAAgC;AACpC,YAAU,QAAQ,KAAK,aAAa,EAAE,EAAE,WAAW,MAAM,CAAC;AAC1D,MAAI,OAAO,YAAY,SACrB,eAAc,KAAK,cAAc,SAAS,QAAQ;MAElD,eAAc,KAAK,cAAc,QAAQ;;CAI7C,WAAmB;EACjB,MAAM,UAAU,KAAK,SAAS,KAAK,MAAM,GAAG,KAAK,YAAY;AAC7D,SAAO,WAAW,SAAS,CAAC,OAAO,QAAQ,CAAC,OAAO,MAAM;;CAG3D,OAAe;AACb,SAAO,SAAS,KAAK,aAAa,CAAC;;;;;AC9DvC,MAAM,cAAc;AAOpB,IAAa,cAAb,MAAyB;CACvB;CAEA,YAAY,MAAc;AACxB,OAAK,WAAW,KAAK,MAAM,KAAK,MAAM,YAAY,CAAC;;CAGrD,OAAO,cAA+B;EACpC,IAAI,SAAS;AACb,OAAK,MAAM,EAAE,SAAS,aAAa,KAAK,SACtC,KAAI,KAAK,MAAM,SAAS,aAAa,CACnC,UAAS,CAAC;AAGd,SAAO;;CAGT,MAAc,UAA6B;AACzC,MAAI,CAAC,WAAW,SAAS,CAAE,QAAO,EAAE;AACpC,SAAO,aAAa,UAAU,QAAQ,CACnC,MAAM,KAAK,CACX,KAAK,MAAM,EAAE,MAAM,CAAC,CACpB,QAAQ,MAAM,KAAK,CAAC,EAAE,WAAW,IAAI,CAAC,CACtC,KAAK,MAAM;GACV,MAAM,UAAU,EAAE,WAAW,IAAI;GACjC,IAAI,UAAU,UAAU,EAAE,MAAM,EAAE,GAAG;AACrC,OAAI,QAAQ,WAAW,IAAI,CAAE,WAAU,QAAQ,MAAM,EAAE;AACvD,UAAO;IAAE;IAAS;IAAS;IAC3B;;CAGN,MAAc,SAAiB,MAAuB;AACpD,MAAI,QAAQ,SAAS,IAAI,CACvB,QAAO,KAAK,WAAW,QAAQ,IAAI,SAAS,QAAQ,MAAM,GAAG,GAAG;AAElE,MAAI,QAAQ,SAAS,IAAI,CACvB,QAAO,KAAK,QAAQ,SAAS,KAAK;AAEpC,SAAO,KAAK,QAAQ,SAAS,KAAK,IAAI,KAAK,QAAQ,SAAS,SAAS,KAAK,CAAC;;CAG7E,QAAgB,SAAiB,KAAsB;EACrD,MAAM,KAAK,QACR,MAAM,KAAK,CACX,KAAK,MACJ,EACG,QAAQ,qBAAqB,OAAO,CACpC,QAAQ,OAAO,QAAQ,CACvB,QAAQ,OAAO,OAAO,CAC1B,CACA,KAAK,KAAK;AACb,SAAO,IAAI,OAAO,IAAI,GAAG,GAAG,CAAC,KAAK,IAAI;;;;;ACxD1C,MAAM,gBAAgB;CAAC;CAAa;CAAU;CAAS;AAEvD,IAAa,YAAb,MAAuB;CACrB;CACA;CAEA,YAAY,MAAc;AACxB,OAAK,OAAO,QAAQ,KAAK;AACzB,OAAK,SAAS,IAAI,YAAY,KAAK,KAAK;;CAG1C,UAAmB;AACjB,SAAO,cAAc,MAAM,MAAM;AAC/B,OAAI;AACF,WAAO,SAAS,KAAK,KAAK,MAAM,EAAE,CAAC,CAAC,aAAa;WAC3C;AACN,WAAO;;IAET;;CAGJ,QAAqB;AACnB,SAAO,KAAK,KAAK,KAAK,KAAK,CAAC,QACzB,MAAM,CAAC,KAAK,OAAO,OAAO,EAAE,aAAa,CAC3C;;CAGH,KAAK,YAA2C;AAC9C,MAAI,sBAAsB,UAAW,QAAO;AAI5C,SAAO,IAAI,UAHC,WAAW,WAAW,GAC9B,aACA,KAAK,KAAK,MAAM,WAAW,EACL,KAAK,KAAK;;CAGtC,KAAa,KAA0B;EACrC,MAAM,UAAuB,EAAE;AAC/B,OAAK,MAAM,SAAS,YAAY,KAAK,EAAE,eAAe,MAAM,CAAC,EAAE;AAC7D,OAAI,MAAM,KAAK,WAAW,IAAI,CAAE;GAChC,MAAM,OAAO,KAAK,KAAK,MAAM,KAAK;AAClC,OAAI,MAAM,aAAa,CACrB,SAAQ,KAAK,GAAG,KAAK,KAAK,KAAK,CAAC;YACvB,MAAM,QAAQ,CACvB,SAAQ,KAAK,IAAI,UAAU,MAAM,KAAK,KAAK,CAAC;;AAGhD,SAAO;;;;;ACjDX,IAAa,YAAb,MAAuB;CACrB,4BAAoB,IAAI,KAAqB;CAE7C,IAAI,KAA2B;AAC7B,MAAI,UAAU,KAAK;GACjB,gBAAgB;GAChB,iBAAiB;GACjB,YAAY;GACZ,+BAA+B;GAChC,CAAC;AACF,MAAI,MAAM,QAAQ;AAClB,OAAK,UAAU,IAAI,IAAI;AACvB,MAAI,GAAG,eAAe,KAAK,UAAU,OAAO,IAAI,CAAC;;CAGnD,UAAU,MAAoB;EAC5B,MAAM,UAAU,SAAS,KAAK;AAC9B,OAAK,MAAM,OAAO,KAAK,UACrB,KAAI;AACF,OAAI,MAAM,QAAQ;UACZ;AACN,QAAK,UAAU,OAAO,IAAI;;;CAKhC,QAAc;AACZ,OAAK,MAAM,OAAO,KAAK,UACrB,KAAI;AACF,OAAI,KAAK;UACH;AAIV,OAAK,UAAU,OAAO;;CAGxB,IAAI,OAAe;AACjB,SAAO,KAAK,UAAU;;;;;ACxC1B,SAAgB,qBAAqB,MAAmC;AACtE,QAAO;;;+BAGsB,KAAK,UAAU,EAAE,MAAM,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDxD,SAAgB,gBACd,MACA,MACQ;CACR,MAAM,SAAS,qBAAqB,KAAK;AACzC,KAAI,KAAK,SAAS,UAAU,CAC1B,QAAO,KAAK,QAAQ,WAAW,GAAG,OAAO,WAAW;AAEtD,QAAO,OAAO;;;;AC1DhB,MAAM,aAAa,IAAI,IAAI;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AASF,eAAsB,aACpB,KACA,KACA,MACe;CACf,MAAM,cAAc,GAAG,KAAK,QAAQ;CAEpC,MAAM,UAAkC,EAAE;AAC1C,MAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,IAAI,QAAQ,CAC9C,KAAI,CAAC,WAAW,IAAI,EAAE,aAAa,CAAC,IAAI,OAAO,MAAM,SACnD,SAAQ,KAAK;AAGjB,SAAQ,UAAU;AAClB,SAAQ,mBAAmB,OAAO,KAAK,QAAQ;AAC/C,SAAQ,gBAAgB;AACxB,SAAQ,qBAAqB;CAE7B,MAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,IAAI,QAAQ,OAAO;AACjE,KAAI,aAAa,IAAI,OAAO,IAAI;AAChC,KAAI,aAAa,IAAI,MAAM,IAAI;CAE/B,MAAM,UAAU,KAAK,gBAAgB,IAAI,EAAE;CAC3C,MAAM,QAAQ,IAAI,WAAW,SAAS,IAAI,WAAW;CACrD,IAAI,SAAS,IAAI,UAAU;CAC3B,IAAI;AAEJ,KAAI,QAAQ,SAAS,KAAK,OAAO;AAC/B,WAAS;EACT,MAAM,SAAS,IAAI,iBAAiB;AACpC,SAAO,IAAI,WAAW,IAAI,UAAU,MAAM;AAC1C,OAAK,MAAM,KAAK,QACd,QAAO,IAAI,qBAAqB,EAAE,aAAa,IAAI,EAAE,MAAM,CAAC;EAE9D,MAAM,QAAQ,cAAc;AAC5B,MAAI,MAAO,SAAQ,mBAAmB,UAAU;AAChD,UAAQ,kBAAkB;AAC1B,SAAO,OAAO,UAAU;AACxB,UAAQ,oBAAoB,OAAO,OAAO,WAAW,KAAK,CAAC;YAClD,CAAC,OAAO;AACjB,SAAO,MAAM,SAAS,IAAI;AAC1B,MAAI,KAAK,SAAS,EAChB,SAAQ,oBAAoB,OAAO,KAAK,OAAO;;AAInD,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,UAAgC;GACpC,UAAU;GACV,MAAM;GACN,MAAM,IAAI,YAAY,IAAI,UAAU;GACpC;GACA;GACD;EAED,MAAM,WAAW,MAAM,QAAQ,UAAU,aAAa;GAEpD,MAAM,UADc,SAAS,QAAQ,mBAAmB,IAC7B,SAAS,YAAY;GAEhD,MAAM,kBAAqD,EAAE;AAC7D,QAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,SAAS,QAAQ,CACnD,KAAI,CAAC,WAAW,IAAI,EAAE,aAAa,CAAC,IAAI,MAAM,KAAA,EAC5C,iBAAgB,KAAK;AAIzB,OAAI,QAAQ;IACV,MAAM,SAAmB,EAAE;AAC3B,aAAS,GAAG,SAAS,UAAkB,OAAO,KAAK,MAAM,CAAC;AAC1D,aAAS,GAAG,aAAa;KACvB,IAAI,OAAO,OAAO,OAAO,OAAO,CAAC,SAAS,QAAQ;AAClD,YAAO,gBAAgB,MAAM,KAAK,WAAW;AAC7C,qBAAgB,oBAAoB,OAAO,OAAO,WAAW,KAAK,CAAC;AACnE,SAAI,UAAU,SAAS,cAAc,KAAK,gBAAgB;AAC1D,SAAI,IAAI,KAAK;AACb,cAAS;MACT;UACG;AACL,QAAI,UAAU,SAAS,cAAc,KAAK,gBAAgB;AAC1D,aAAS,KAAK,IAAI;AAClB,aAAS,GAAG,OAAO,QAAQ;;IAE7B;AAEF,WAAS,GAAG,UAAU,QAAQ;AAC5B,UAAO,IAAI;IACX;AAEF,MAAI,KAAM,UAAS,MAAM,KAAK;AAC9B,WAAS,KAAK;GACd;;AAGJ,SAAS,SAAS,KAAuC;AACvD,QAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAmB,EAAE;AAC3B,MAAI,GAAG,SAAS,UAAkB,OAAO,KAAK,MAAM,CAAC;AACrD,MAAI,GAAG,aAAa,QAAQ,OAAO,OAAO,OAAO,CAAC,CAAC;AACnD,MAAI,GAAG,SAAS,OAAO;GACvB;;;;AChHJ,SAAgB,WACd,MACA,SACqB;CACrB,MAAM,UAAU,SAAS,MAAM,KAAK,MAAM;EACxC,eAAe;EACf,UAAU,aAAqB;AAC7B,OAAI,SAAS,SAAS,eAAe,CAAE,QAAO;AAC9C,OAAI;IACF,MAAM,MAAM,SAAS,KAAK,MAAM,SAAS;AAEzC,YADiB,IAAI,MAAM,QAAQ,CAAC,KAAK,IAAI,IAC7B,WAAW,IAAI,IAAI,KAAK,OAAO,OAAO,IAAI;WACpD;AACN,WAAO;;;EAGX,YAAY;EACZ,kBAAkB;GAAE,oBAAoB;GAAI,cAAc;GAAI;EAC/D,CAAC;CAEF,IAAI,UAAU,QAAQ,SAAS;CAC/B,MAAM,WAAW,OAA4B;AAC3C,YAAU,QAAQ,KAAK,GAAG,CAAC,YAAY,GAAG;;AAG5C,SAAQ,GAAG,WAAW,aAAa;EACjC,MAAM,MAAM,SAAS,KAAK,MAAM,SAAS;AACzC,MAAI,KAAK,OAAO,OAAO,IAAI,CAAE;AAC7B,gBAAc,QAAQ,CAAC,KAAK,KAAK,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;GACrD;AAEF,SAAQ,GAAG,QAAQ,aAAa;EAC9B,MAAM,MAAM,SAAS,KAAK,MAAM,SAAS;AACzC,MAAI,KAAK,OAAO,OAAO,IAAI,CAAE;AAC7B,gBAAc,QAAQ,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC;GACrD;AAEF,SAAQ,GAAG,WAAW,aAAa;AACjC,gBAAc,QAAQ,EAAE,EAAE,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC;GACrD;AAEF,cAAa,QAAQ,OAAO;;;;AChC9B,IAAa,SAAb,MAAoB;CAClB,4BAAoB,IAAI,KAAqB;CAE7C,YACE,KACA,SACA,WACA;AAHQ,OAAA,MAAA;AACA,OAAA,UAAA;AACA,OAAA,YAAA;;CAKV,MAAM,iBAAgC;EACpC,MAAM,OAAO,MAAM,KAAK,IAAI,IAEzB,2BAA2B,KAAK,QAAQ,YAAY;AACvD,OAAK,gBAAgB,KAAK,+BAA+B,EAAE,CAAC;;CAG9D,gBAAwB,WAAmC;AACzD,OAAK,MAAM,KAAK,UACd,KAAI,EAAE,IAAK,MAAK,UAAU,IAAI,EAAE,KAAK,EAAE,SAAS;AAElD,OAAK,MAAM,OAAO,KAAK,UAAU,MAAM,CACrC,KAAI,KAAK,UAAU,IAAI,GAAG,IAAI,SAAS,CAAE,MAAK,UAAU,OAAO,IAAI;;CAIvE,WAAW,MAA0B;AACnC,SAAO,KAAK,UAAU,KAAK,KAAK,UAAU,IAAI,KAAK,aAAa;;CAGlE,aAAuB;AACrB,SAAO,CAAC,GAAG,KAAK,UAAU,MAAM,CAAC;;CAKnC,MAAM,WAAW,MAAgC;EAC/C,MAAM,OAAO,2BAA2B,KAAK,QAAQ;AACrD,MAAI,KAAK,OACP,OAAM,KAAK,IAAI,IAAI,MAAM,EACvB,4BAA4B;GAC1B,KAAK,KAAK;GACV,SAAS,KAAK,MAAM;GACrB,EACF,CAAC;MAEF,OAAM,KAAK,iBAAiB,MAAM,KAAK;;CAI3C,MAAc,iBACZ,MACA,cACe;EAWf,MAAM,SATkB,MAAM,KAAK,IAAI,KAEpC,mBAAmB,EACpB,mBAAmB;GACjB,aAAa,2BAA2B,KAAK;GAC7C,WAAW,KAAK,KAAK;GACrB,MAAM,KAAK;GACZ,EACF,CAAC,EAC4B;EAG9B,MAAM,WAAW,MAAM,KAAK,IAAI,KAI7B,iCAAiC,EAAE,CAAC;EAGvC,MAAM,SAAS,KAAK,8BAA8B,MAAM,eAAe;EACvE,MAAM,WAAW,IAAI,UAAU;EAC/B,MAAM,OAAO,IAAI,KAAK,CAAC,KAAK,YAAY,CAA2B,EAAE,EACnE,MAAM,KAAK,KAAK,MACjB,CAAC;AACF,WAAS,OAAO,QAAQ,MAAM,KAAK,KAAK;AACxC,WAAS,OAAO,SAAS,SAAS,MAAM;AACxC,WAAS,OAAO,aAAa,SAAS,UAAU;AAChD,WAAS,OAAO,UAAU,OAAO,SAAS,OAAO,CAAC;AAClD,WAAS,OAAO,UAAU,OAAO;AACjC,WAAS,OAAO,YAAY,KAAK,KAAK;AACtC,WAAS,OAAO,aAAa,sCAAsC;EAEnE,MAAM,SAAS,MAAM,MACnB,kDACA;GACE,QAAQ;GACR,MAAM;GACP,CACF;AACD,MAAI,CAAC,OAAO,GAAI,OAAM,IAAI,MAAM,2BAA2B,OAAO,SAAS;EAC3E,MAAM,SAAU,MAAM,OAAO,MAAM;EAUnC,MAAM,kBAA2C,EAC/C,OAAO;GACL,IAAI,MAAM;GACV,kBAAkB,OAAO;GACzB,cAAc,OAAO;GACrB,WAAW,KAAK,KAAK;GACrB,MAAM,KAAK;GACX,WAAW,OAAO;GAClB,eAAe,MAAM;GACtB,EACF;AACD,MAAI,OAAO,OACR,iBAAgB,SAAqC,YACpD,OAAO;AACX,MAAI,OAAO,MACR,iBAAgB,SAAqC,WACpD,OAAO;EAEX,MAAM,eAAe,MAAM,KAAK,IAAI,KAEjC,qCAAqC,gBAAgB;AAGxD,QAAM,KAAK,IAAI,IAAI,cAAc,EAC/B,4BAA4B;GAC1B,KAAK,KAAK;GACV,WAAW;IACT,gBAAgB,aAAa,MAAM;IACnC,cAAc,KAAK,KAAK;IACxB,cAAc,OAAO;IACrB,UAAU,KAAK;IACf,QAAQ,aAAa,MAAM;IAC3B,KAAK,aAAa,MAAM;IACxB,mBAAmB,OAAO;IAC3B;GACF,EACF,CAAC;;CAGJ,8BAAsC,eAA+B;EACnE,MAAM,QAAQ,cAAc,MAAM,IAAI;EACtC,MAAM,YAAY,MAAM,MAAM;EAC9B,MAAM,WAAW,MAAM,MAAM;EAC7B,MAAM,YAAY,MAAM,MAAM;AAQ9B,SAAO,GAAG,UAAU,GAPsB;GACxC,QAAQ;GACR,QAAQ;GACR,OAAO;GACP,WAAW;GACX,OAAO;GACR,CACgC,aAAa,QAAQ,GAAG;;CAK3D,MAAM,iBAAiB,cAAqC;AAC1D,QAAM,KAAK,IAAI,OAAO,2BAA2B,KAAK,QAAQ,aAAa,EACzE,MAAM,EAAE,4BAA4B,EAAE,KAAK,cAAc,EAAE,EAC5D,CAAC;AACF,OAAK,UAAU,OAAO,aAAa;;CAKrC,MAAM,cAAyC;EAC7C,MAAM,OAAO,MAAM,KAAK,IAAI,IAEzB,2BAA2B,KAAK,QAAQ,YAAY;AACvD,OAAK,gBAAgB,KAAK,+BAA+B,EAAE,CAAC;AAC5D,SAAO,KAAK,+BAA+B,EAAE;;CAG/C,MAAM,oBAAoB,KAA8B;EACtD,MAAM,OAAO,MAAM,MAAM,IAAI;AAC7B,MAAI,CAAC,KAAK,GAAI,OAAM,IAAI,MAAM,6BAA6B,KAAK,SAAS;AACzE,SAAO,OAAO,KAAK,MAAM,KAAK,aAAa,CAAC;;CAK9C,MAAM,YACJ,OAGI,EAAE,EACe;AACrB,QAAM,KAAK,gBAAgB;EAE3B,MAAM,aAAa,KAAK,UAAU,OAAO;EACzC,MAAM,SAAqB;GACzB,UAAU;GACV,SAAS;GACT,YAAY;GACZ,QAAQ,EAAE;GACX;EAED,MAAM,WAAW,WAAW,QAAQ,MAAM,EAAE,UAAU,KAAK,WAAW,EAAE,CAAC;EACzE,IAAI,OAAO;AACX,OAAK,MAAM,QAAQ,UAAU;AAC3B,OAAI;AACF,UAAM,KAAK,WAAW,KAAK;AAC3B,WAAO;YACA,GAAG;AACV,WAAO,OAAO,KAAK,UAAU,KAAK,aAAa,IAAI,IAAI;;AAEzD,QAAK,aAAa,EAAE,MAAM,SAAS,OAAO;;AAG5C,MAAI,KAAK,QAAQ;GACf,MAAM,aAAa,IAAI,IAAI,WAAW,KAAK,MAAM,EAAE,aAAa,CAAC;GACjE,MAAM,WAAW,KAAK,YAAY,CAAC,QAAQ,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC;AACpE,QAAK,MAAM,OAAO,SAChB,KAAI;AACF,UAAM,KAAK,iBAAiB,IAAI;AAChC,WAAO;YACA,GAAG;AACV,WAAO,OAAO,KAAK,UAAU,IAAI,IAAI,IAAI;;;AAK/C,SAAO;;CAKT,MAAM,cACJ,OAGI,EAAE,EACe;EACrB,MAAM,YAAY,MAAM,KAAK,aAAa;EAC1C,MAAM,SAAqB;GACzB,UAAU;GACV,SAAS;GACT,YAAY;GACZ,QAAQ,EAAE;GACX;EAED,IAAI,OAAO;AACX,OAAK,MAAM,YAAY,WAAW;GAChC,MAAM,OAAO,KAAK,UAAU,KAAK,SAAS,IAAI;AAG9C,OAAI,CAAC,KAAK,aAAa,WAAW,KAAK,UAAU,OAAO,IAAI,EAAE;AAC5D,WAAO,OAAO,KAAK,YAAY,SAAS,IAAI,2BAA2B;AACvE,SAAK,aAAa,EAAE,MAAM,UAAU,OAAO;AAC3C;;AAGF,OAAI;AACF,QAAI,SAAS,kBAAkB,kBAAkB,SAAS,KAAK;KAC7D,MAAM,MAAM,MAAM,KAAK,oBAAoB,SAAS,IAAI;AACxD,UAAK,MAAM,IAAI;eACN,SAAS,YAAY,KAAA,EAC9B,MAAK,MAAM,SAAS,QAAQ;AAE9B,WAAO;YACA,GAAG;AACV,WAAO,OAAO,KAAK,YAAY,SAAS,IAAI,IAAI,IAAI;;AAEtD,QAAK,aAAa,EAAE,MAAM,UAAU,OAAO;;AAG7C,MAAI,KAAK,QAAQ;GACf,MAAM,aAAa,IAAI,IAAI,UAAU,KAAK,MAAM,EAAE,IAAI,CAAC;AACvD,QAAK,MAAM,QAAQ,KAAK,UAAU,OAAO,CACvC,KAAI,CAAC,WAAW,IAAI,KAAK,aAAa,CACpC,KAAI;IACF,MAAM,EAAE,eAAe,MAAM,OAAO;AACpC,eAAW,KAAK,aAAa;AAC7B,WAAO;WACD;;AAOd,SAAO;;;;;AC9RX,eAAsB,eACpB,KACA,OACA,WACA,MACA,SACqB;CACrB,MAAM,MAAM,IAAI,WAAW;CAC3B,MAAM,SAAS,IAAI,OAAO,KAAK,MAAM,IAAI,UAAU;CAEnD,MAAM,iCAAiB,IAAI,KAAa;AAGxC,SAAQ,IAAI,mBAAmB,MAAM,KAAK,KAAK,MAAM,GAAG,IAAI;AAC5D,OAAM,OAAO,YAAY;EACvB,QAAQ;EACR,aAAa,MAAM,UAAU;AAC3B,WAAQ,OAAO,MAAM,iBAAiB,KAAK,GAAG,MAAM,SAAS;;EAEhE,CAAC;AACF,SAAQ,OAAO,MAAM,KAAK;CAG1B,MAAM,cAAc,WAClB,WACA,OAAO,UAAU,OAAO,YAAY;EAClC,MAAM,UAAU,CAAC,GAAG,UAAU,GAAG,MAAM;AAEvC,OAAK,MAAM,QAAQ,SAAS;AAC1B,kBAAe,IAAI,KAAK,aAAa;AACrC,OAAI;AACF,UAAM,OAAO,WAAW,KAAK;YACtB,GAAG;AACV,YAAQ,MACN,8BAA8B,KAAK,aAAa,IAAI,IACrD;aACO;AACR,mBAAe,OAAO,KAAK,aAAa;;;AAI5C,OAAK,MAAM,QAAQ,QACjB,KAAI;AACF,SAAM,OAAO,iBAAiB,KAAK,aAAa;UAC1C;AAKV,MAAI,QAAQ,SAAS,EACnB,KAAI,UAAU,KAAK,UAAU,EAAE,aAAa,MAAM,CAAC,CAAC;WAC3C,QAAQ,SAAS,EAC1B,KAAI,UACF,KAAK,UAAU,EAAE,UAAU,QAAQ,KAAK,MAAM,EAAE,aAAa,EAAE,CAAC,CACjE;GAGN;CAGD,MAAM,SAAS,KAAK,aAAa,OAAO,KAAK,QAAQ;AACnD,MAAI,IAAI,QAAQ,eAAe;AAC7B,OAAI,IAAI,IAAI;AACZ;;AAGF,MAAI;AACF,SAAM,aAAa,KAAK,KAAK;IAC3B,SAAS,MAAM;IACf,SAAS,MAAM;IACf,YAAY,KAAK;IACjB,oBACE,CAAC,GAAG,eAAe,CAChB,KAAK,MAAM,UAAU,KAAK,EAAE,CAAC,CAC7B,QAAQ,MAAM,EAAE,OAAO,CACvB,KAAK,OAAO;KACX,cAAc,EAAE;KAChB,YAAY,EAAE,MAAM;KACrB,EAAE;IACR,CAAC;WACK,GAAG;AACV,WAAQ,MAAM,WAAW,IAAI,OAAO,GAAG,IAAI,IAAI,KAAK,IAAI;AACxD,OAAI,CAAC,IAAI,aAAa;AACpB,QAAI,UAAU,IAAI;AAClB,QAAI,IAAI,cAAc;;;GAG1B;AAEF,OAAM,IAAI,SAAe,SAAS,WAAW;AAC3C,SAAO,OAAO,KAAK,MAAM,KAAK,YAAY,SAAS,CAAC;AACpD,SAAO,GAAG,SAAS,OAAO;GAC1B;CAEF,MAAM,UAAU,UAAU,KAAK,KAAK,GAAG,KAAK;AAC5C,WAAU,QAAQ;AAGlB,QAAO,SAAS,OAAO;AACrB,MAAI,OAAO;AACX,eAAa;AACb,SAAO,OAAO;;;;;AC1GlB,eAAe,eACb,KACA,YAC2B;AAC3B,KAAI,YAAY;EACd,MAAM,OAAO,MAAM,IAAI,IACrB,0BACD;EACD,MAAM,SACH,KAAK,sBAAsB,EAAE,EAAE,MAC7B,MAAM,OAAO,EAAE,GAAG,KAAK,WACzB,KACA,KAAK,sBAAsB,EAAE,EAAE,MAC7B,MAAM,EAAE,KAAK,aAAa,KAAK,WAAW,aAAa,CACzD;AACH,MAAI,CAAC,OAAO;AACV,WAAQ,MAAM,oBAAoB,aAAa;AAC/C,WAAQ,KAAK,EAAE;;AAEjB,SAAO;;CAIT,MAAM,EAAE,eAAe,gBAAgB;AACvC,KAAI,WACF,KAAI;EACF,MAAM,OAAO,MAAM,IAAI,IACrB,2BAA2B,aAC5B;AACD,MAAI,KAAK,mBAAmB;AAC1B,WAAQ,IAAI,6BAA6B,aAAa;AACtD,UAAO,KAAK;;SAER;CAMV,MAAM,EAAE,aAAa,MAAM,OAAO;CAElC,MAAM,OACJ,gBAFW,UAAU,CAAC,MAAM,IAAI,CAAC,MAAM,MAElB,GAAG,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,GAAG,MAChE,GACA,GACD;CAMH,MAAM,SAJO,MAAM,IAAI,KACrB,2BACA,EAAE,mBAAmB;EAAE;EAAM,MAAM;EAAe,EAAE,CACrD,EACkB;AACnB,gBAAe;EAAE,YAAY,MAAM;EAAI,cAAc,MAAM;EAAM,CAAC;AAClE,SAAQ,IAAI,sBAAsB,MAAM,KAAK,KAAK,MAAM,GAAG,GAAG;AAC9D,QAAO;;AAGT,SAAgB,mBAA4B;AAC1C,QAAO,IAAI,QAAQ,MAAM,CACtB,YAAY,6CAA6C,CACzD,OAAO,iBAAiB,qBAAqB,YAAY,CACzD,OAAO,iBAAiB,qBAAqB,OAAO,CACpD,OACC,4BACA,6CACD,CACA,OAAO,eAAe,mCAAmC,CACzD,OAAO,wBAAwB,gCAAgC,YAAY,CAC3E,OAAO,cAAc,6CAA6C,CAClE,OAAO,iBAAiB,wBAAwB,IAAI,CACpD,OACC,OAAO,SAQD;AACJ,gBAAc;EAEd,MAAM,YAAY,IAAI,UAAU,KAAK,KAAK;AAC1C,MAAI,CAAC,UAAU,SAAS,EAAE;AACxB,WAAQ,MAAM,IAAI,KAAK,KAAK,yCAAyC;AACrE,WAAQ,KAAK,EAAE;;EAGjB,MAAM,OAAO,OAAO,KAAK,KAAK;AAC9B,MAAI,CAAC,OAAO,UAAU,KAAK,IAAI,OAAO,KAAK,OAAO,OAAO;AACvD,WAAQ,MACN,kBAAkB,KAAK,KAAK,4CAC7B;AACD,WAAQ,KAAK,EAAE;;EAGjB,MAAM,aAAa,KAAK,eAAe,QAAQ,QAAQ;EACvD,MAAM,MAAM,iBAAiB;EAK7B,MAAM,WAHa,MAAM,IAAI,IAC3B,+BACD,EAC0B,MAAM,SAAS;AAC1C,MAAI,CAAC,SAAS;AACZ,WAAQ,MACN,wEACD;AACD,WAAQ,KAAK,EAAE;;EAGjB,MAAM,QAAQ,MAAM,eAAe,KAAK,KAAK,MAAM;EAEnD,IAAI;EAEJ,MAAM,gBAAgB;AACpB,WAAQ;AACR,WAAQ,KAAK,EAAE;;AAEjB,UAAQ,GAAG,UAAU,QAAQ;AAC7B,UAAQ,GAAG,WAAW,QAAQ;AAE9B,SAAO,MAAM,eACX,KACA;GACE,IAAI,MAAM;GACV,MAAM,MAAM;GACZ;GACA,WAAW,MAAM;GAClB,EACD,WACA;GAAE,MAAM,KAAK;GAAM;GAAM;GAAY,GACpC,YAAY;AACX,WAAQ,IAAI,mBAAmB,UAAU;AACzC,OAAI,MAAM,WACR,SAAQ,IAAI,iBAAiB,MAAM,aAAa;AAClD,WAAQ,IAAI,mCAAmC;AAE/C,OAAI,KAAK,SACP,QAAO,QAAQ,MAAM,MAAM,EAAE,QAAQ,GAAG,QAAQ,OAAO,CAAC;IAG7D;AAGD,QAAM,IAAI,cAAc,GAAG;GAE9B;;;;ACtJL,eAAe,YACb,KAC2B;CAI3B,MAAM,UAHO,MAAM,IAAI,IACrB,0BACD,EACmB,sBAAsB,EAAE;AAC5C,KAAI,CAAC,OAAO,QAAQ;AAClB,UAAQ,MAAM,mBAAmB;AACjC,UAAQ,KAAK,EAAE;;CAEjB,MAAM,EAAE,OAAO,MAAM,QACnB;EACE,MAAM;EACN,MAAM;EACN,SAAS;EACT,SAAS,OAAO,KAAK,OAAO;GAC1B,OAAO,GAAG,EAAE,KAAK,KAAK,EAAE,GAAG;GAC3B,OAAO,EAAE;GACV,EAAE;EACJ,EACD,EAAE,gBAAgB,QAAQ,KAAK,IAAI,EAAE,CACtC;AACD,KAAI,CAAC,IAAI;AACP,UAAQ,MAAM,qBAAqB;AACnC,UAAQ,KAAK,EAAE;;AAEjB,QAAO,OAAO,MAAM,MAAM,EAAE,OAAO,GAAG;;AAGxC,eAAe,UACb,KACA,YAC2B;CAI3B,MAAM,UAHO,MAAM,IAAI,IACrB,0BACD,EACmB,sBAAsB,EAAE;CAC5C,MAAM,QACJ,OAAO,MAAM,MAAM,OAAO,EAAE,GAAG,KAAK,WAAW,IAC/C,OAAO,MAAM,MAAM,EAAE,KAAK,aAAa,KAAK,WAAW,aAAa,CAAC;AACvE,KAAI,CAAC,OAAO;AACV,UAAQ,MAAM,mCAAmC,aAAa;AAC9D,UAAQ,KAAK,EAAE;;AAEjB,QAAO;;AAGT,SAAgB,oBAA6B;AAC3C,QAAO,IAAI,QAAQ,OAAO,CACvB,YAAY,2CAA2C,CACvD,OAAO,4BAA4B,8BAA8B,CACjE,OAAO,kBAAkB,6CAA6C,CACtE,OAAO,eAAe,yBAAyB,CAC/C,OAAO,iBAAiB,kCAAkC,CAC1D,OAAO,iBAAiB,wBAAwB,IAAI,CACpD,OACC,OAAO,SAMD;AACJ,gBAAc;EAEd,MAAM,YAAY,IAAI,UAAU,KAAK,KAAK;AAC1C,MAAI,CAAC,UAAU,SAAS,EAAE;AACxB,WAAQ,MAAM,IAAI,KAAK,KAAK,yCAAyC;AACrE,WAAQ,KAAK,EAAE;;EAGjB,MAAM,MAAM,iBAAiB;EAC7B,MAAM,QAAQ,KAAK,QACf,MAAM,UAAU,KAAK,KAAK,MAAM,GAChC,MAAM,YAAY,IAAI;EAE1B,MAAM,SAAS,IAAI,OAAO,KAAK,MAAM,IAAI,UAAU;EACnD,MAAM,UAAU,IAAI,cAAc,MAAM,KAAK,KAAK,MAAM,GAAG,IAAI,CAAC,OAAO;EAEvE,MAAM,SAAS,MAAM,OAAO,YAAY;GACtC,QAAQ,CAAC,KAAK;GACd,aAAa,GAAG,UAAU;AACxB,YAAQ,OAAO,WAAW,EAAE,GAAG,MAAM;;GAExC,CAAC;AAEF,MAAI,OAAO,OAAO,QAAQ;AACxB,WAAQ,KAAK,eAAe,OAAO,OAAO,OAAO,YAAY;AAC7D,QAAK,MAAM,KAAK,OAAO,OAAQ,SAAQ,MAAM,KAAK,IAAI;QAEtD,SAAQ,QACN,UAAU,OAAO,SAAS,oBAAoB,OAAO,QAAQ,kBAC9D;AAGH,MAAI,KAAK,SAAS;GAChB,MAAM,aAAa,IAAI,oBAAoB,CAAC,OAAO;AACnD,OAAI;AACF,UAAM,IAAI,KAAK,2BAA2B,MAAM,GAAG,UAAU;AAC7D,eAAW,QAAQ,mBAAmB;YAC/B,GAAG;AACV,eAAW,KAAK,mBAAmB,IAAI;;;GAI9C;;;;AC1GL,eAAe,kBACb,KACA,YAC2B;CAI3B,MAAM,UAHO,MAAM,IAAI,IACrB,0BACD,EACmB,sBAAsB,EAAE;AAC5C,KAAI,CAAC,OAAO,QAAQ;AAClB,UAAQ,MAAM,mBAAmB;AACjC,UAAQ,KAAK,EAAE;;AAGjB,KAAI,YAAY;EACd,MAAM,QACJ,OAAO,MAAM,MAAM,OAAO,EAAE,GAAG,KAAK,WAAW,IAC/C,OAAO,MAAM,MAAM,EAAE,KAAK,aAAa,KAAK,WAAW,aAAa,CAAC;AACvE,MAAI,CAAC,OAAO;AACV,WAAQ,MAAM,mCAAmC,aAAa;AAC9D,WAAQ,KAAK,EAAE;;AAEjB,SAAO;;CAGT,MAAM,EAAE,OAAO,MAAM,QACnB;EACE,MAAM;EACN,MAAM;EACN,SAAS;EACT,SAAS,OAAO,KAAK,OAAO;GAC1B,OAAO,GAAG,EAAE,KAAK,KAAK,EAAE,GAAG;GAC3B,OAAO,EAAE;GACV,EAAE;EACJ,EACD,EAAE,gBAAgB,QAAQ,KAAK,IAAI,EAAE,CACtC;AACD,KAAI,CAAC,IAAI;AACP,UAAQ,MAAM,qBAAqB;AACnC,UAAQ,KAAK,EAAE;;AAEjB,QAAO,OAAO,MAAM,MAAM,EAAE,OAAO,GAAG;;AAGxC,SAAgB,oBAA6B;AAC3C,QAAO,IAAI,QAAQ,OAAO,CACvB,YAAY,8CAA8C,CAC1D,OAAO,4BAA4B,2BAA2B,CAC9D,OAAO,kBAAkB,8CAA8C,CACvE,OAAO,iBAAiB,wBAAwB,IAAI,CACpD,OACC,OAAO,SAA+D;AACpE,gBAAc;EAEd,MAAM,MAAM,iBAAiB;EAC7B,MAAM,QAAQ,MAAM,kBAAkB,KAAK,KAAK,MAAM;EACtD,MAAM,YAAY,IAAI,UAAU,KAAK,KAAK;EAE1C,MAAM,SAAS,IAAI,OAAO,KAAK,MAAM,IAAI,UAAU;EACnD,MAAM,UAAU,IAAI,WAAW,MAAM,KAAK,KAAK,MAAM,GAAG,IAAI,CAAC,OAAO;EAEpE,MAAM,SAAS,MAAM,OAAO,cAAc;GACxC,QAAQ,CAAC,KAAK;GACd,aAAa,GAAG,UAAU;AACxB,YAAQ,OAAO,eAAe,EAAE,GAAG,MAAM;;GAE5C,CAAC;AAEF,MAAI,OAAO,OAAO,QAAQ;AACxB,WAAQ,KAAK,eAAe,OAAO,OAAO,OAAO,YAAY;AAC7D,QAAK,MAAM,KAAK,OAAO,OAAQ,SAAQ,MAAM,KAAK,IAAI;QAEtD,SAAQ,QACN,cAAc,OAAO,WAAW,oBAAoB,OAAO,QAAQ,iBACpE;GAGN;;;;AClFL,MAAM,oBAAoB;AAE1B,MAAM,eAAe;AAErB,SAAgB,oBAA6B;AAC3C,QAAO,IAAI,QAAQ,OAAO,CACvB,YAAY,mDAAmD,CAC/D,SAAS,UAAU,mCAAmC,CACtD,OAAO,yBAAyB,yBAAyB,kBAAkB,CAC3E,OAAO,OAAO,MAA0B,SAA+B;AACtE,MAAI,CAAC,MAAM;AAST,WARY,MAAM,QAChB;IACE,MAAM;IACN,MAAM;IACN,SAAS;IACV,EACD,EAAE,gBAAgB,QAAQ,KAAK,IAAI,EAAE,CACtC,EACU;AACX,OAAI,CAAC,MAAM;AACT,YAAQ,MAAM,oBAAoB;AAClC,YAAQ,KAAK,EAAE;;;AAInB,MAAI,CAAC,aAAa,KAAK,KAAK,EAAE;AAC5B,WAAQ,MACN,wBAAwB,KAAK,+DAC9B;AACD,WAAQ,KAAK,EAAE;;AAGjB,UAAQ,IAAI,sBAAsB,KAAK,SAAS,QAAQ,KAAK,GAAG;AAChE,eAAa,OAAO;GAAC;GAAS,KAAK;GAAU;GAAK,EAAE,EAAE,OAAO,WAAW,CAAC;AAEzE,OAAK,MAAM,OAAO,CAAC,QAAQ,UAAU,EAAE;GACrC,MAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,OAAI,WAAW,KAAK,CAAE,QAAO,MAAM;IAAE,WAAW;IAAM,OAAO;IAAM,CAAC;;AAGtE,UAAQ,IAAI,4BAA4B,OAAO;AAC/C,UAAQ,IAAI,qBAAqB,KAAK,sBAAsB;GAC5D;;;;AC5CN,MAAM,gBAAgB;CACpB;EAAE,OAAO;EAAQ,MAAM;EAAS;CAChC;EAAE,OAAO;EAAQ,MAAM;EAAc;CACrC;EAAE,OAAO;EAAkB,MAAM;EAAc;CAC/C;EAAE,OAAO;EAAQ,MAAM;EAAS;CAChC;EAAE,OAAO;EAAQ,MAAM;EAAc;CACrC;EAAE,OAAO;EAAoB,MAAM;EAAoB;CACvD;EAAE,OAAO;EAAqB,MAAM;EAAqB;CAC1D;AAED,MAAM,kBAAkB;CACtB;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACD;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACD;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACD;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACD;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACD;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACD;EACE,OAAO;EACP,MAAM;EACN,UAAU;EACV,UAAU;EACX;CACF;AAED,SAAgB,wBAAiC;AAC/C,QAAO,IAAI,QAAQ,WAAW,CAC3B,YAAY,8DAA8D,CAC1E,OAAO,iBAAiB,mBAAmB,YAAY,CACvD,OAAO,iBAAiB,mBAAmB,OAAO,CAClD,OAAO,oBAAoB,0CAA0C,CACrE,OAAO,OAAO,SAAyD;AACtE,gBAAc;EAEd,MAAM,UAAU,KAAK,QACjB,OAAO,KAAK,MAAM,GAClB,gBAAgB,CAAC;AAErB,MAAI,CAAC,SAAS;AACZ,WAAQ,MACN,0EACD;AACD,WAAQ,KAAK,EAAE;;EAGjB,MAAM,UAAU,UAAU,KAAK,KAAK,GAAG,KAAK;EAa5C,MAAM,UAAoB,CACxB,GAAG,cAAc,KAAK,OAAO;GAAE,OAAO,EAAE;GAAO,OAAO,EAAE;GAAM,EAAE,EAChE,GAAG,gBAAgB,KAAK,OAAO;GAC7B,OAAO,GAAG,EAAE,MAAM;GAClB,OAAO;IACL,cAAc,EAAE;IAChB,UAAU,EAAE;IACZ,UAAU,EAAE;IACZ,OAAO,EAAE;IACV;GACF,EAAE,CACJ;EAED,MAAM,iBAAiB,QAAQ,KAAK,IAAI;EAExC,MAAM,EAAE,SAAS,MAAM,QACrB;GACE,MAAM;GACN,MAAM;GACN,SAAS;GACT;GACD,EACD,EAAE,UAAU,CACb;AAED,MAAI,CAAC,KAAM;EAEX,IAAI;AACJ,MAAI,OAAO,SAAS,SAClB,QAAO;OACF;GAQL,MAAM,aANO,MADD,iBAAiB,CACN,IAEpB,2BAA2B,QAAQ,wBAAwB;IAC5D,WAAW,KAAK;IAChB,UAAU;IACX,CAAC,EACqB,wBAAwB,EAAE;AAEjD,OAAI,CAAC,UAAU,QAAQ;AACrB,YAAQ,IAAI,MAAM,KAAK,MAAM,uCAAuC;AACpE,WAAO,KAAK;UACP;IACL,MAAM,EAAE,SAAS,MAAM,QACrB;KACE,MAAM;KACN,MAAM;KACN,SAAS,YAAY,KAAK,MAAM,aAAa;KAC7C,SAAS,UAAU,KAAK,OAAO;MAC7B,OAAO,EAAE,SAAS,EAAE;MACpB,OAAO,EAAE;MACV,EAAE;KACJ,EACD,EAAE,UAAU,CACb;AACD,WAAO,KAAK,SAAS,QAAQ,MAAM,KAAe;;;EAItD,MAAM,MAAM,GAAG,UAAU;AACzB,UAAQ,IAAI,oBAAoB,IAAI,IAAI;EACxC,MAAM,QAAQ,MAAM,OAAO,SAAS;AACpC,QAAM,KAAK,IAAI;GACf;;;;ACrJN,SAAgB,qBAAqB,KAA0B;CAC7D,MAAM,MAAM,IAAI,QAAQ,QAAQ,CAAC,YAC/B,0DACD;AAED,KAAI,WAAW,kBAAkB,CAAC;AAClC,KAAI,WAAW,mBAAmB,CAAC;AACnC,KAAI,WAAW,mBAAmB,CAAC;AACnC,KAAI,WAAW,mBAAmB,CAAC;AACnC,KAAI,WAAW,uBAAuB,CAAC;AAEvC,KAAI,QAAQ,WAAW,IAAI;;;;AChB7B,MAAM,SAAsB;CAC1B,MAAM;CACN,SAAS;CACT,SAAS,KAAoB;AAC3B,uBAAqB,IAAI;;CAE5B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluid-app/fluid-cli-theme-dev",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Fluid CLI plugin for theme developer workflows — dev server, push, pull, init",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -22,7 +22,7 @@
22
22
  "open": "^10.0.0",
23
23
  "ora": "^8.0.0",
24
24
  "prompts": "^2.4.2",
25
- "@fluid-app/fluid-cli": "0.1.2"
25
+ "@fluid-app/fluid-cli": "0.1.3"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^24",
@@ -7,10 +7,13 @@ import { startDevServer } from "../theme/dev-server/index.js";
7
7
  interface ApplicationTheme {
8
8
  id: number;
9
9
  name: string;
10
- company: string;
11
10
  editor_url?: string;
12
11
  }
13
12
 
13
+ interface CompanyMe {
14
+ data: { company: { subdomain?: string; name?: string } };
15
+ }
16
+
14
17
  async function ensureDevTheme(
15
18
  api: ReturnType<typeof createApiClient>,
16
19
  identifier?: string,
@@ -62,7 +65,6 @@ async function ensureDevTheme(
62
65
  "/api/application_themes",
63
66
  { application_theme: { name, role: "development" } },
64
67
  );
65
-
66
68
  const theme = body.application_theme;
67
69
  setPluginState({ devThemeId: theme.id, devThemeName: theme.name });
68
70
  console.log(`Created dev theme: ${theme.name} (#${theme.id})`);
@@ -100,8 +102,28 @@ export function createDevCommand(): Command {
100
102
  process.exit(1);
101
103
  }
102
104
 
105
+ const port = Number(opts.port);
106
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
107
+ console.error(
108
+ `Invalid port: '${opts.port}'. Must be an integer between 1 and 65535.`,
109
+ );
110
+ process.exit(1);
111
+ }
112
+
103
113
  const reloadMode = opts.liveReload === "off" ? "off" : "full-page";
104
114
  const api = createApiClient();
115
+
116
+ const companyRes = await api.get<CompanyMe>(
117
+ "/api/company/v1/companies/me",
118
+ );
119
+ const company = companyRes.data?.company?.subdomain;
120
+ if (!company) {
121
+ console.error(
122
+ "Could not determine company subdomain. Make sure your token is valid.",
123
+ );
124
+ process.exit(1);
125
+ }
126
+
105
127
  const theme = await ensureDevTheme(api, opts.theme);
106
128
 
107
129
  let stop: (() => void) | undefined;
@@ -113,20 +135,12 @@ export function createDevCommand(): Command {
113
135
  process.on("SIGINT", cleanup);
114
136
  process.on("SIGTERM", cleanup);
115
137
 
116
- const port = Number(opts.port);
117
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
118
- console.error(
119
- `Invalid port: '${opts.port}'. Must be an integer between 1 and 65535.`,
120
- );
121
- process.exit(1);
122
- }
123
-
124
138
  stop = await startDevServer(
125
139
  api,
126
140
  {
127
141
  id: theme.id,
128
142
  name: theme.name,
129
- company: theme.company,
143
+ company,
130
144
  editorUrl: theme.editor_url,
131
145
  },
132
146
  themeRoot,
@@ -8,7 +8,6 @@ import { Syncer } from "../theme/syncer.js";
8
8
  interface ApplicationTheme {
9
9
  id: number;
10
10
  name: string;
11
- company: string;
12
11
  }
13
12
 
14
13
  async function selectTheme(
package/src/theme/root.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readdirSync, statSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
3
  import { ThemeFile } from "./file.js";
4
4
  import { FluidIgnore } from "./fluid-ignore.js";
5
5
 
@@ -32,7 +32,10 @@ export class ThemeRoot {
32
32
 
33
33
  file(pathOrFile: string | ThemeFile): ThemeFile {
34
34
  if (pathOrFile instanceof ThemeFile) return pathOrFile;
35
- return new ThemeFile(join(this.root, pathOrFile), this.root);
35
+ const abs = isAbsolute(pathOrFile)
36
+ ? pathOrFile
37
+ : join(this.root, pathOrFile);
38
+ return new ThemeFile(abs, this.root);
36
39
  }
37
40
 
38
41
  private glob(dir: string): ThemeFile[] {