@fluid-app/fluid-cli-theme-dev 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +7 -5
- package/dist/index.mjs +67 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/commands/dev.ts +3 -3
- package/src/commands/navigate.ts +104 -8
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @fluid-app/fluid-cli-theme-dev@0.1.
|
|
2
|
+
> @fluid-app/fluid-cli-theme-dev@0.1.10 build /home/runner/_work/fluid-mono/fluid-mono/packages/cli/theme-dev
|
|
3
3
|
> tsdown
|
|
4
4
|
|
|
5
5
|
[34mℹ[39m tsdown [2mv0.21.0[22m powered by rolldown [2mv1.0.0-rc.7[22m
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
[34mℹ[39m target: [34mnode18[39m
|
|
9
9
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
10
10
|
[34mℹ[39m Build start
|
|
11
|
-
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m
|
|
12
|
-
[34mℹ[39m [2mdist/[22mindex.mjs.map [
|
|
11
|
+
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m 45.06 kB[22m [2m│ gzip: 12.79 kB[22m
|
|
12
|
+
[34mℹ[39m [2mdist/[22mindex.mjs.map [2m118.77 kB[22m [2m│ gzip: 26.94 kB[22m
|
|
13
13
|
[34mℹ[39m [2mdist/[22mindex.d.mts.map [2m 0.11 kB[22m [2m│ gzip: 0.12 kB[22m
|
|
14
14
|
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 0.19 kB[22m [2m│ gzip: 0.16 kB[22m
|
|
15
|
-
[34mℹ[39m 4 files, total:
|
|
16
|
-
[
|
|
15
|
+
[34mℹ[39m 4 files, total: 164.13 kB
|
|
16
|
+
[33m[PLUGIN_TIMINGS] Warning:[0m Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
|
|
17
|
+
|
|
18
|
+
[32m✔[39m Build complete in [32m4607ms[39m
|
package/dist/index.mjs
CHANGED
|
@@ -37,7 +37,7 @@ var ApiError = class ApiError extends Error {
|
|
|
37
37
|
* Creates a configured fetch client instance
|
|
38
38
|
*/
|
|
39
39
|
function createFetchClient(config) {
|
|
40
|
-
const { baseUrl, getAuthToken, onAuthError, defaultHeaders = {} } = config;
|
|
40
|
+
const { baseUrl, getAuthToken, onAuthError, defaultHeaders = {}, credentials } = config;
|
|
41
41
|
/**
|
|
42
42
|
* Build headers for a request
|
|
43
43
|
*/
|
|
@@ -126,6 +126,7 @@ function createFetchClient(config) {
|
|
|
126
126
|
method,
|
|
127
127
|
headers
|
|
128
128
|
};
|
|
129
|
+
if (credentials) fetchOptions.credentials = credentials;
|
|
129
130
|
const serializedBody = body && method !== "GET" ? JSON.stringify(body) : null;
|
|
130
131
|
if (serializedBody) fetchOptions.body = serializedBody;
|
|
131
132
|
if (signal) fetchOptions.signal = signal;
|
|
@@ -150,6 +151,7 @@ function createFetchClient(config) {
|
|
|
150
151
|
headers,
|
|
151
152
|
body: formData
|
|
152
153
|
};
|
|
154
|
+
if (credentials) fetchOptions.credentials = credentials;
|
|
153
155
|
if (signal) fetchOptions.signal = signal;
|
|
154
156
|
response = await fetch(url, fetchOptions);
|
|
155
157
|
} catch (networkError) {
|
|
@@ -1093,6 +1095,7 @@ function createDevCommand() {
|
|
|
1093
1095
|
process.exit(1);
|
|
1094
1096
|
}
|
|
1095
1097
|
const theme = await ensureDevTheme(api, opts.theme);
|
|
1098
|
+
const editorUrl = `https://admin.fluid.app/themes/${theme.id}/editor`;
|
|
1096
1099
|
let stop;
|
|
1097
1100
|
const cleanup = () => {
|
|
1098
1101
|
stop?.();
|
|
@@ -1104,14 +1107,14 @@ function createDevCommand() {
|
|
|
1104
1107
|
id: theme.id,
|
|
1105
1108
|
name: theme.name,
|
|
1106
1109
|
company,
|
|
1107
|
-
editorUrl
|
|
1110
|
+
editorUrl
|
|
1108
1111
|
}, themeRoot, {
|
|
1109
1112
|
host: opts.host,
|
|
1110
1113
|
port,
|
|
1111
1114
|
reloadMode
|
|
1112
1115
|
}, (address) => {
|
|
1113
1116
|
console.log(`\n Dev server: ${address}`);
|
|
1114
|
-
|
|
1117
|
+
console.log(` Web editor: ${editorUrl}`);
|
|
1115
1118
|
console.log("\n Watching for file changes…\n");
|
|
1116
1119
|
if (opts.navigate) import("open").then((m) => m.default(`${address}/home`));
|
|
1117
1120
|
});
|
|
@@ -1231,6 +1234,20 @@ function createInitCommand() {
|
|
|
1231
1234
|
}
|
|
1232
1235
|
//#endregion
|
|
1233
1236
|
//#region src/commands/navigate.ts
|
|
1237
|
+
function localSuggest(input, choices) {
|
|
1238
|
+
if (!input) return choices;
|
|
1239
|
+
const lower = input.toLowerCase();
|
|
1240
|
+
return choices.filter((c) => c.title.toLowerCase().includes(lower));
|
|
1241
|
+
}
|
|
1242
|
+
const THEMEABLE_TYPE_MAP = {
|
|
1243
|
+
"/home": "home_page",
|
|
1244
|
+
"/home/shop": "shop_page",
|
|
1245
|
+
"/home/join": "join_page",
|
|
1246
|
+
"/cart": "cart_page",
|
|
1247
|
+
"/home/blog": "post_page",
|
|
1248
|
+
"/home/categories": "category_page",
|
|
1249
|
+
"/home/collections": "collection_page"
|
|
1250
|
+
};
|
|
1234
1251
|
const STATIC_ROUTES = [
|
|
1235
1252
|
{
|
|
1236
1253
|
label: "Home",
|
|
@@ -1311,6 +1328,29 @@ const RESOURCE_ROUTES = [
|
|
|
1311
1328
|
fallback: "/home/pages"
|
|
1312
1329
|
}
|
|
1313
1330
|
];
|
|
1331
|
+
async function fetchTemplatesForType(api, themeId, themeableType) {
|
|
1332
|
+
const params = new URLSearchParams({
|
|
1333
|
+
application_theme_id: String(themeId),
|
|
1334
|
+
themeable_type: themeableType,
|
|
1335
|
+
published: "true"
|
|
1336
|
+
});
|
|
1337
|
+
return (await api.get(`/api/application_theme_templates?${params}`)).templates ?? [];
|
|
1338
|
+
}
|
|
1339
|
+
async function selectTemplate(api, themeId, themeableType, onCancel) {
|
|
1340
|
+
const templates = await fetchTemplatesForType(api, themeId, themeableType);
|
|
1341
|
+
if (templates.length <= 1) return null;
|
|
1342
|
+
const { templateId } = await prompts({
|
|
1343
|
+
type: "autocomplete",
|
|
1344
|
+
name: "templateId",
|
|
1345
|
+
message: "Select a template",
|
|
1346
|
+
choices: templates.map((t) => ({
|
|
1347
|
+
title: `${t.name}${t.default ? " (default)" : ""}`,
|
|
1348
|
+
value: t.id
|
|
1349
|
+
})),
|
|
1350
|
+
suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
|
|
1351
|
+
}, { onCancel });
|
|
1352
|
+
return templateId ?? null;
|
|
1353
|
+
}
|
|
1314
1354
|
function createNavigateCommand() {
|
|
1315
1355
|
return new Command("navigate").description("Interactively navigate to a route in the dev server browser").option("--host <host>", "Dev server host", "127.0.0.1").option("--port <port>", "Dev server port", "9292").option("-t, --theme <id>", "Theme ID (defaults to active dev theme)").action(async (opts) => {
|
|
1316
1356
|
requireToken();
|
|
@@ -1334,16 +1374,22 @@ function createNavigateCommand() {
|
|
|
1334
1374
|
}))];
|
|
1335
1375
|
const onCancel = () => process.exit(130);
|
|
1336
1376
|
const { dest } = await prompts({
|
|
1337
|
-
type: "
|
|
1377
|
+
type: "autocomplete",
|
|
1338
1378
|
name: "dest",
|
|
1339
1379
|
message: "Select a route",
|
|
1340
|
-
choices
|
|
1380
|
+
choices,
|
|
1381
|
+
suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
|
|
1341
1382
|
}, { onCancel });
|
|
1342
1383
|
if (!dest) return;
|
|
1384
|
+
const api = createApiClient();
|
|
1343
1385
|
let path;
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1386
|
+
let themeableType;
|
|
1387
|
+
if (typeof dest === "string") {
|
|
1388
|
+
path = dest;
|
|
1389
|
+
themeableType = THEMEABLE_TYPE_MAP[dest];
|
|
1390
|
+
} else {
|
|
1391
|
+
themeableType = dest.resourceType;
|
|
1392
|
+
const resources = (await getApplicationThemeAvailableThemeables(api, themeId, {
|
|
1347
1393
|
themeable: dest.resourceType,
|
|
1348
1394
|
per_page: 50
|
|
1349
1395
|
})).available_themeables ?? [];
|
|
@@ -1351,19 +1397,26 @@ function createNavigateCommand() {
|
|
|
1351
1397
|
console.log(`No ${dest.label} resources found, using listing page.`);
|
|
1352
1398
|
path = dest.fallback;
|
|
1353
1399
|
} else {
|
|
1400
|
+
const resourceChoices = resources.map((r) => ({
|
|
1401
|
+
title: r.title ?? r.slug ?? "Untitled",
|
|
1402
|
+
value: r.slug
|
|
1403
|
+
}));
|
|
1354
1404
|
const { slug } = await prompts({
|
|
1355
|
-
type: "
|
|
1405
|
+
type: "autocomplete",
|
|
1356
1406
|
name: "slug",
|
|
1357
1407
|
message: `Select a ${dest.label.toLowerCase()}`,
|
|
1358
|
-
choices:
|
|
1359
|
-
|
|
1360
|
-
value: r.slug
|
|
1361
|
-
}))
|
|
1408
|
+
choices: resourceChoices,
|
|
1409
|
+
suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
|
|
1362
1410
|
}, { onCancel });
|
|
1363
1411
|
path = dest.template.replace("%s", slug);
|
|
1364
1412
|
}
|
|
1365
1413
|
}
|
|
1366
|
-
|
|
1414
|
+
let templateParam = "";
|
|
1415
|
+
if (themeableType) {
|
|
1416
|
+
const templateId = await selectTemplate(api, themeId, themeableType, onCancel);
|
|
1417
|
+
if (templateId) templateParam = `?theme_template_id=${templateId}`;
|
|
1418
|
+
}
|
|
1419
|
+
const url = `${address}${path}${templateParam}`;
|
|
1367
1420
|
console.log(`\nNavigating to: ${url}\n`);
|
|
1368
1421
|
const open = (await import("open")).default;
|
|
1369
1422
|
await open(url);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["themes.listThemeResources","themes.updateThemeResource","themes.deleteThemeResource","themes.listApplicationThemes","themes.getApplicationTheme","themes.getApplicationTheme","themes.createApplicationTheme","themes.createApplicationTheme","themes.publishApplicationTheme","themes.getApplicationThemeAvailableThemeables"],"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","../../../api-clients/themes/src/namespaces/v0.ts","../src/theme/syncer.ts","../src/theme/dev-server/index.ts","../src/theme-picker.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 {\n createFetchClient,\n type FetchClient,\n} from \"@fluid-app/api-client-core\";\nimport { getAuthToken } from \"@fluid-app/fluid-cli\";\n\nexport type ApiClient = FetchClient;\n\n/** Base URL for all API calls. Set FLUID_API_BASE to route through a BFF. */\nfunction getApiBase(): string {\n return process.env[\"FLUID_API_BASE\"] ?? \"https://api.fluid.app\";\n}\n\nexport function createApiClient(tokenOverride?: string): ApiClient {\n return createFetchClient({\n baseUrl: getApiBase(),\n getAuthToken: () => tokenOverride ?? getAuthToken() ?? null,\n });\n}\n\nexport function requireToken(): string {\n const token = getAuthToken();\n if (!token) {\n console.error(\"Not logged in. Run `fluid login` first.\");\n process.exit(1);\n }\n return token;\n}\n","import { 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","/**\n * Generated API client functions for v0\n *\n * DO NOT EDIT THIS FILE DIRECTLY\n * This file is auto-generated. To update:\n * 1. Update the OpenAPI spec file\n * 2. Run: pnpm generate\n */\n\nimport type { FetchClient } from \"../lib/fetch-client\";\nimport type { operations } from \"../generated/v0\";\n\n// ============================================================================\n// applicationthemetemplates\n// ============================================================================\n\n/**\n * Lists all theme templates\n * \n *\n * @param client - Fetch client instance\n \n */\nexport async function listThemeTemplates(\n client: FetchClient,\n): Promise<\n operations[\"listThemeTemplates\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_theme_templates`);\n}\n\n/**\n * Creates a theme template\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createThemeTemplate(\n client: FetchClient,\n body: NonNullable<\n operations[\"createThemeTemplate\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createThemeTemplate\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates`, body);\n}\n\n/**\n * List all mysite themes\n * List all mysite themes\n *\n * @param client - Fetch client instance\n \n */\nexport async function listMysiteThemes(\n client: FetchClient,\n): Promise<\n operations[\"listMysiteThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_theme_templates/mysite_themes`);\n}\n\n/**\n * Retrieves a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_theme_templates/${id}`);\n}\n\n/**\n * Updates a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateThemeTemplate(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateThemeTemplate\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/application_theme_templates/${id}`, body);\n}\n\n/**\n * Deletes a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"deleteThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/application_theme_templates/${id}`);\n}\n\n/**\n * Returns all available themeables for theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeTemplateAvailableThemeables(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeTemplateAvailableThemeables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_theme_templates/${id}/available_themeables`,\n );\n}\n\n/**\n * Get available variables for a theme template\n * Get available variables that can be used in the theme template\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeTemplateAvailableVariables(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeTemplateAvailableVariables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_theme_templates/${id}/available_variables`,\n );\n}\n\n/**\n * Clones a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function cloneThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"cloneThemeTemplate\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/clone`);\n}\n\n/**\n * Publishes the template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function publishThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"publishThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/publish`);\n}\n\n/**\n * Renders a page for a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function renderThemeTemplatePage(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"renderThemeTemplatePage\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"renderThemeTemplatePage\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(\n `/api/application_theme_templates/${id}/render_page`,\n body,\n );\n}\n\n/**\n * Renders a section template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function renderThemeTemplateSection(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"renderThemeTemplateSection\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/render_section`);\n}\n\n/**\n * Sets a theme template as default\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function setDefaultThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"setDefaultThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/set_default`);\n}\n\n/**\n * Updates themeable records to be used by the specified template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function updateThemeTemplateThemeables(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"updateThemeTemplateThemeables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.put(`/api/application_theme_templates/${id}/themeables_update`);\n}\n\n// ============================================================================\n// application-themes\n// ============================================================================\n\n/**\n * List application themes\n * Get all application themes with optional filters\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listApplicationThemes(\n client: FetchClient,\n params?: operations[\"listApplicationThemes\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listApplicationThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes`, params);\n}\n\n/**\n * Create an application theme\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createApplicationTheme(\n client: FetchClient,\n body: NonNullable<\n operations[\"createApplicationTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createApplicationTheme\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes`, body);\n}\n\n/**\n * Get current active application theme\n * \n *\n * @param client - Fetch client instance\n \n */\nexport async function getActiveApplicationTheme(\n client: FetchClient,\n): Promise<\n operations[\"getActiveApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes/active`);\n}\n\n/**\n * Import an application theme from zip file\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function importApplicationThemeFromZip(\n client: FetchClient,\n body: NonNullable<\n operations[\"importApplicationThemeFromZip\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"importApplicationThemeFromZip\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/import_zip`, body);\n}\n\n/**\n * Get an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param [params] - params\n */\nexport async function getApplicationTheme(\n client: FetchClient,\n id: string | number,\n params?: operations[\"getApplicationTheme\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"getApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes/${id}`, params);\n}\n\n/**\n * Update an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateApplicationTheme(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateApplicationTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/application_themes/${id}`, body);\n}\n\n/**\n * Delete an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"deleteApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/application_themes/${id}`);\n}\n\n/**\n * Returns available themeables for a given type scoped to the theme's company\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param [params] - params\n */\nexport async function getApplicationThemeAvailableThemeables(\n client: FetchClient,\n id: string | number,\n params?: operations[\"getApplicationThemeAvailableThemeables\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"getApplicationThemeAvailableThemeables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_themes/${id}/available_themeables`,\n params,\n );\n}\n\n/**\n * Clone an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function cloneApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"cloneApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/${id}/clone`);\n}\n\n/**\n * Import an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function importApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"importApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/${id}/import`);\n}\n\n/**\n * Publishes the theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function publishApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"publishApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/${id}/publish`);\n}\n\n/**\n * Get theme assets\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeAssets(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeAssets\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes/${id}/theme_assets`);\n}\n\n// ============================================================================\n// applicationthemeresources\n// ============================================================================\n\n/**\n * Lists all theme resources\n *\n *\n * @param client - Fetch client instance\n * @param application_theme_id - application_theme_id\n */\nexport async function listThemeResources(\n client: FetchClient,\n application_theme_id: string | number,\n): Promise<\n operations[\"listThemeResources\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_themes/${application_theme_id}/resources`,\n );\n}\n\n/**\n * Updates a theme resource\n *\n *\n * @param client - Fetch client instance\n * @param application_theme_id - application_theme_id\n * @param body - body\n */\nexport async function updateThemeResource(\n client: FetchClient,\n application_theme_id: string | number,\n body: NonNullable<\n operations[\"updateThemeResource\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateThemeResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.put(\n `/api/application_themes/${application_theme_id}/resources`,\n body,\n );\n}\n\n/**\n * Deletes a theme resource\n *\n *\n * @param client - Fetch client instance\n * @param application_theme_id - application_theme_id\n * @param body - body\n */\nexport async function deleteThemeResource(\n client: FetchClient,\n application_theme_id: string | number,\n body: NonNullable<\n operations[\"deleteThemeResource\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"deleteThemeResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(\n `/api/application_themes/${application_theme_id}/resources`,\n { body },\n );\n}\n\n// ============================================================================\n// file-resources\n// ============================================================================\n\n/**\n * Returns a list of file resources\n *\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listFileResources(\n client: FetchClient,\n params?: operations[\"listFileResources\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listFileResources\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/file_resources`, params);\n}\n\n/**\n * Creates a file resource\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createFileResource(\n client: FetchClient,\n body: NonNullable<\n operations[\"createFileResource\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createFileResource\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/file_resources`, body);\n}\n\n/**\n * Creates multiple file resources\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function bulkCreateFileResources(\n client: FetchClient,\n body: NonNullable<\n operations[\"bulkCreateFileResources\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"bulkCreateFileResources\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/file_resources/bulk_create`, body);\n}\n\n/**\n * Deletes multiple file resources\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function bulkDestroyFileResources(\n client: FetchClient,\n body: NonNullable<\n operations[\"bulkDestroyFileResources\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"bulkDestroyFileResources\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/file_resources/bulk_destroy`, { body });\n}\n\n/**\n * Shows a file resource\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function showFileResource(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"showFileResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/file_resources/${id}`);\n}\n\n/**\n * Deletes a file resource\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function destroyFileResource(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"destroyFileResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/file_resources/${id}`);\n}\n\n// ============================================================================\n// root-themes\n// ============================================================================\n\n/**\n * List root themes\n * Get all root themes with optional filters\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listRootThemes(\n client: FetchClient,\n params?: operations[\"listRootThemes\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listRootThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/root_themes`, params);\n}\n\n/**\n * Create a root theme\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createRootTheme(\n client: FetchClient,\n body: NonNullable<\n operations[\"createRootTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createRootTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/root_themes`, body);\n}\n\n/**\n * List company root themes\n * Get all company root themes with optional filters\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listCompanyRootThemes(\n client: FetchClient,\n params?: operations[\"listCompanyRootThemes\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listCompanyRootThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/root_themes/my`, params);\n}\n\n/**\n * Update a root theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateRootTheme(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateRootTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateRootTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/root_themes/${id}`, body);\n}\n\n/**\n * Delete a root theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteRootTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"deleteRootTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/root_themes/${id}`);\n}\n\n/**\n * Update a root theme status\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateRootThemeStatus(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateRootThemeStatus\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateRootThemeStatus\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/root_themes/${id}/status`, body);\n}\n\n// ============================================================================\n// theme-region-rules\n// ============================================================================\n\n/**\n * List theme region rules\n * Retrieve a list of theme region rules for the current company\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listThemeRegionRules(\n client: FetchClient,\n params?: operations[\"listThemeRegionRules\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listThemeRegionRules\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/theme_region_rules`, params);\n}\n\n/**\n * Create theme region rule\n * Create a new theme region rule\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createThemeRegionRule(\n client: FetchClient,\n body: NonNullable<\n operations[\"createThemeRegionRule\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createThemeRegionRule\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/theme_region_rules`, body);\n}\n\n/**\n * Show theme region rule\n * Retrieve a specific theme region rule\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeRegionRule(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeRegionRule\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/theme_region_rules/${id}`);\n}\n\n/**\n * Update theme region rule\n * Update an existing theme region rule\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateThemeRegionRule(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateThemeRegionRule\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateThemeRegionRule\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/theme_region_rules/${id}`, body);\n}\n\n/**\n * Delete theme region rule\n * Delete a theme region rule\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteThemeRegionRule(\n client: FetchClient,\n id: string | number,\n): Promise<void> {\n return client.delete(`/api/theme_region_rules/${id}`);\n}\n","import { sep } from \"node:path\";\nimport type { ApiClient } from \"../api.js\";\nimport type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport { themes, type components } from \"@fluid-app/themes-api-client\";\n\ntype RemoteResource = components[\"schemas\"][\"ApplicationThemeResource\"];\n\nexport interface SyncResult {\n uploaded: number;\n downloaded: number;\n deleted: number;\n errors: string[];\n}\n\nexport class Syncer {\n private checksums = new Map<string, string>();\n\n constructor(\n private api: ApiClient,\n private themeId: number,\n private themeRoot: ThemeRoot,\n ) {}\n\n // ─── Checksum Management ──────────────────────────────────────────────────\n\n async fetchChecksums(): Promise<void> {\n const body = await themes.listThemeResources(this.api, this.themeId);\n this.updateChecksums(body.application_theme_resources ?? []);\n }\n\n private updateChecksums(resources: RemoteResource[]): void {\n for (const r of resources) {\n if (r.key && r.checksum) this.checksums.set(r.key, r.checksum);\n }\n for (const key of this.checksums.keys()) {\n if (this.checksums.has(`${key}.liquid`)) this.checksums.delete(key);\n }\n }\n\n hasChanged(file: ThemeFile): boolean {\n return file.checksum() !== this.checksums.get(file.relativePath);\n }\n\n remoteKeys(): string[] {\n return [...this.checksums.keys()];\n }\n\n // ─── Upload ───────────────────────────────────────────────────────────────\n\n async uploadFile(file: ThemeFile): Promise<void> {\n if (file.isText) {\n await themes.updateThemeResource(this.api, this.themeId, {\n application_theme_resource: {\n key: file.relativePath,\n content: file.read(),\n },\n });\n } else {\n await this.uploadBinaryFile(file);\n }\n }\n\n private async uploadBinaryFile(file: ThemeFile): Promise<void> {\n // Step 1: Create DAM placeholder\n const placeholderBody = await this.api.post<{\n asset: { id: number; canonical_path: string };\n }>(\"/api/dam/assets\", {\n placeholder_asset: {\n description: `Uploaded via Fluid CLI: ${file.name}`,\n mime_type: file.mime.name,\n name: file.name,\n },\n });\n const asset = placeholderBody.asset;\n\n // Step 2: Get ImageKit auth token\n const authBody = await this.api.post<{\n token: string;\n signature: string;\n expire: number;\n }>(\"/api/dam/assets/imagekit_auth\", {});\n\n // Step 3: Upload to ImageKit via multipart\n const folder = this.canonicalPathToImageKitFolder(asset.canonical_path);\n const formData = new FormData();\n const blob = new Blob([file.readBinary() as unknown as ArrayBuffer], {\n type: file.mime.name,\n });\n formData.append(\"file\", blob, file.name);\n formData.append(\"token\", authBody.token);\n formData.append(\"signature\", authBody.signature);\n formData.append(\"expire\", String(authBody.expire));\n formData.append(\"folder\", folder);\n formData.append(\"fileName\", file.name);\n formData.append(\"publicKey\", \"public_j7s4Ih9ETh/OCp41mVQH7tlXBdU=\");\n\n const ikResp = await fetch(\n \"https://upload.imagekit.io/api/v1/files/upload\",\n {\n method: \"POST\",\n body: formData,\n },\n );\n if (!ikResp.ok) throw new Error(`ImageKit upload failed: ${ikResp.status}`);\n const ikBody = (await ikResp.json()) as {\n fileId: string;\n url: string;\n thumbnailUrl: string;\n size: number;\n height?: number;\n width?: number;\n };\n\n // Step 4: Backfill DAM asset\n const backfillPayload: Record<string, unknown> = {\n asset: {\n id: asset.id,\n imagekit_file_id: ikBody.fileId,\n imagekit_url: ikBody.url,\n mime_type: file.mime.name,\n name: file.name,\n file_size: ikBody.size,\n expected_path: asset.canonical_path,\n },\n };\n if (ikBody.height)\n (backfillPayload[\"asset\"] as Record<string, unknown>)[\"height\"] =\n ikBody.height;\n if (ikBody.width)\n (backfillPayload[\"asset\"] as Record<string, unknown>)[\"width\"] =\n ikBody.width;\n\n const backfillBody = await this.api.post<{\n asset: { code: string; default_variant_url: string };\n }>(\"/api/dam/assets/backfill_imagekit\", backfillPayload);\n\n // Step 5: Associate with theme resource\n await themes.updateThemeResource(this.api, this.themeId, {\n application_theme_resource: {\n key: file.relativePath,\n dam_asset: {\n dam_asset_code: backfillBody.asset.code,\n content_type: file.mime.name,\n content_size: ikBody.size,\n filename: file.name,\n handle: backfillBody.asset.code,\n url: backfillBody.asset.default_variant_url,\n preview_image_url: ikBody.thumbnailUrl,\n },\n },\n });\n }\n\n private canonicalPathToImageKitFolder(canonicalPath: string): string {\n const parts = canonicalPath.split(\".\");\n const companyId = parts[0] ?? \"unknown\";\n const category = parts[1] ?? \"files\";\n const assetCode = parts[2] ?? \"unknown\";\n const folderMap: Record<string, string> = {\n images: \"images\",\n videos: \"videos\",\n audio: \"audio\",\n documents: \"documents\",\n files: \"files\",\n };\n return `${companyId}/${folderMap[category] ?? \"files\"}/${assetCode}`;\n }\n\n // ─── Delete ───────────────────────────────────────────────────────────────\n\n async deleteRemoteFile(relativePath: string): Promise<void> {\n await themes.deleteThemeResource(this.api, this.themeId, {\n application_theme_resource: { key: relativePath },\n });\n this.checksums.delete(relativePath);\n }\n\n // ─── Download ─────────────────────────────────────────────────────────────\n\n async downloadAll(): Promise<RemoteResource[]> {\n const body = await themes.listThemeResources(this.api, this.themeId);\n const resources = body.application_theme_resources ?? [];\n this.updateChecksums(resources);\n return resources;\n }\n\n async downloadBinaryAsset(url: string): Promise<Buffer> {\n const resp = await fetch(url);\n if (!resp.ok) throw new Error(`Failed to download asset: ${resp.status}`);\n return Buffer.from(await resp.arrayBuffer());\n }\n\n // ─── Full Upload ──────────────────────────────────────────────────────────\n\n async uploadTheme(\n opts: {\n delete?: boolean;\n 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 (\n resource.content !== undefined &&\n resource.content !== null\n ) {\n const content =\n typeof resource.content === \"string\"\n ? resource.content\n : JSON.stringify(resource.content);\n file.write(content);\n }\n result.downloaded++;\n } catch (e) {\n result.errors.push(`Download ${resource.key}: ${e}`);\n }\n opts.onProgress?.(++done, resources.length);\n }\n\n if (opts.delete) {\n const remoteKeys = new Set(resources.map((r) => r.key));\n for (const file of this.themeRoot.files()) {\n if (!remoteKeys.has(file.relativePath)) {\n try {\n const { unlinkSync } = await import(\"node:fs\");\n unlinkSync(file.absolutePath);\n result.deleted++;\n } catch {\n // ignore\n }\n }\n }\n }\n\n return result;\n }\n}\n","import http from \"node:http\";\nimport { SSEStream } from \"./sse.js\";\nimport { proxyRequest } from \"./proxy.js\";\nimport { watchTheme } from \"./watcher.js\";\nimport { Syncer } from \"../syncer.js\";\nimport type { ThemeRoot } from \"../root.js\";\nimport type { ApiClient } from \"../../api.js\";\n\nexport interface DevServerOptions {\n host: string;\n port: number;\n reloadMode: \"full-page\" | \"off\";\n}\n\nexport interface DevServerTheme {\n id: number;\n name: string;\n company: string;\n editorUrl?: string;\n}\n\nexport async function startDevServer(\n api: ApiClient,\n theme: DevServerTheme,\n themeRoot: ThemeRoot,\n opts: DevServerOptions,\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 chalk from \"chalk\";\nimport prompts from \"prompts\";\nimport type { createApiClient } from \"./api.js\";\nimport { themes, type components } from \"@fluid-app/themes-api-client\";\n\nexport type ApplicationTheme = components[\"schemas\"][\"ApplicationTheme\"];\n\nconst PAGE_SIZE = 50;\nconst LOAD_MORE_VALUE = -1;\n\nfunction themeLabel(t: ApplicationTheme): string {\n const active = t.status === \"active\" ? ` ${chalk.green(\"[active]\")}` : \"\";\n return `${t.name} (#${t.id})${active}`;\n}\n\nfunction themeChoices(\n themeList: ApplicationTheme[],\n hasMore: boolean,\n): prompts.Choice[] {\n const choices: prompts.Choice[] = themeList.map((t) => ({\n title: themeLabel(t),\n value: t.id,\n }));\n if (hasMore) {\n choices.push({\n title: chalk.dim(`── Load more themes ──`),\n value: LOAD_MORE_VALUE,\n });\n }\n return choices;\n}\n\nasync function fetchThemesPage(\n api: ReturnType<typeof createApiClient>,\n page: number,\n searchQuery?: string,\n): Promise<{\n themes: ApplicationTheme[];\n hasMore: boolean;\n}> {\n const body = await themes.listApplicationThemes(api, {\n per_page: PAGE_SIZE,\n page,\n ...(searchQuery ? { search_query: searchQuery } : {}),\n });\n const list = body.application_themes ?? [];\n const totalPages = body.meta?.total_pages ?? 1;\n return { themes: list, hasMore: page < totalPages };\n}\n\nexport async function selectTheme(\n api: ReturnType<typeof createApiClient>,\n message: string,\n): Promise<ApplicationTheme> {\n const allThemes: ApplicationTheme[] = [];\n let page = 1;\n let hasMore = true;\n let initialIndex = 0;\n\n // Search cache — persists across suggest calls\n let searchQuery = \"\";\n let searchResults: ApplicationTheme[] = [];\n\n while (true) {\n if (hasMore && allThemes.length < page * PAGE_SIZE) {\n const result = await fetchThemesPage(api, page);\n allThemes.push(...result.themes);\n hasMore = result.hasMore;\n }\n\n if (!allThemes.length) {\n console.error(\"No themes found.\");\n process.exit(1);\n }\n\n const choices = themeChoices(allThemes, hasMore);\n\n const { id } = await prompts(\n {\n type: \"autocomplete\",\n name: \"id\",\n message,\n initial: initialIndex,\n choices,\n suggest: async (input: string, choices: prompts.Choice[]) => {\n if (!input) {\n searchQuery = \"\";\n searchResults = [];\n return choices;\n }\n\n if (input !== searchQuery) {\n searchQuery = input;\n try {\n const result = await fetchThemesPage(api, 1, input);\n searchResults = result.themes;\n } catch {\n searchResults = [];\n }\n }\n\n return searchResults.map((t) => ({\n title: themeLabel(t),\n value: t.id,\n }));\n },\n },\n { onCancel: () => process.exit(130) },\n );\n\n if (id === LOAD_MORE_VALUE) {\n initialIndex = allThemes.length;\n page++;\n continue;\n }\n\n if (!id) {\n console.error(\"No theme selected.\");\n process.exit(1);\n }\n\n // Check loaded themes first, then search results\n const found =\n allThemes.find((t) => t.id === id) ??\n searchResults.find((t) => t.id === id);\n if (found) return found;\n\n // Fetch directly by ID as fallback\n const body = await themes.getApplicationTheme(api, id);\n return body.application_theme;\n }\n}\n\nexport async function findTheme(\n api: ReturnType<typeof createApiClient>,\n identifier: string,\n): Promise<ApplicationTheme> {\n // Try ID lookup first\n const idNum = Number(identifier);\n if (Number.isInteger(idNum) && idNum > 0) {\n try {\n const body = await themes.getApplicationTheme(api, idNum);\n if (body.application_theme) return body.application_theme;\n } catch {\n // Not found by ID, fall through to search\n }\n }\n\n // Search by name via API with pagination\n let page = 1;\n let hasMore = true;\n while (hasMore) {\n const result = await fetchThemesPage(api, page, identifier);\n const found = result.themes.find(\n (t) => t.name.toLowerCase() === identifier.toLowerCase(),\n );\n if (found) return found;\n hasMore = result.hasMore;\n page++;\n }\n\n console.error(`No theme found with identifier: ${identifier}`);\n process.exit(1);\n}\n","import { 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\";\nimport { themes } from \"@fluid-app/themes-api-client\";\nimport { findTheme, type ApplicationTheme } from \"../theme-picker.js\";\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 return findTheme(api, identifier);\n }\n\n // Reuse stored dev theme if it still exists\n const { devThemeId } = getPluginState();\n if (devThemeId) {\n try {\n const body = await themes.getApplicationTheme(api, devThemeId);\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 themes.createApplicationTheme(api, {\n application_theme: { name, status: \"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 ?? undefined,\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\";\nimport { themes } from \"@fluid-app/themes-api-client\";\nimport {\n selectTheme,\n findTheme,\n type ApplicationTheme,\n} from \"../theme-picker.js\";\n\nexport function createPushCommand(): Command {\n return new Command(\"push\")\n .description(\"Push local theme files to a remote theme\")\n .option(\"-t, --theme <name-or-id>\", \"Theme name or ID to push to\")\n .option(\"-n, --nodelete\", \"Do not delete remote files missing locally\")\n .option(\"-f, --force\", \"Skip schema validation\")\n .option(\"-p, --publish\", \"Publish the theme after pushing\")\n .option(\n \"-u, --unpublished\",\n \"Create a new unpublished theme and push to it\",\n )\n .option(\"--root <path>\", \"Theme root directory\", \".\")\n .action(\n async (opts: {\n theme?: string;\n nodelete?: boolean;\n force?: boolean;\n publish?: boolean;\n unpublished?: boolean;\n root: string;\n }) => {\n requireToken();\n\n 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 let theme: ApplicationTheme;\n\n if (opts.unpublished) {\n const { name } = await prompts(\n {\n type: \"text\",\n name: \"name\",\n message: \"Name for the new theme\",\n },\n { onCancel: () => process.exit(130) },\n );\n if (!name) {\n console.error(\"Theme name is required.\");\n process.exit(1);\n }\n const body = await themes.createApplicationTheme(api, {\n application_theme: { name, status: \"draft\" },\n });\n theme = body.application_theme;\n console.log(\n `Created unpublished theme: ${theme.name} (#${theme.id})`,\n );\n } else {\n theme = opts.theme\n ? await findTheme(api, opts.theme)\n : await selectTheme(api, \"Select a theme to push to\");\n }\n\n const syncer = new Syncer(api, theme.id, themeRoot);\n const spinner = ora(`Pushing to ${theme.name} (#${theme.id})…`).start();\n\n const result = await syncer.uploadTheme({\n delete: !opts.nodelete,\n 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 themes.publishApplicationTheme(api, theme.id);\n pubSpinner.succeed(\"Theme published.\");\n } catch (e) {\n pubSpinner.fail(`Publish failed: ${e}`);\n }\n }\n },\n );\n}\n","import { Command } from \"commander\";\nimport ora from \"ora\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { Syncer } from \"../theme/syncer.js\";\nimport { selectTheme, findTheme } from \"../theme-picker.js\";\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 = opts.theme\n ? await findTheme(api, opts.theme)\n : await selectTheme(api, \"Select a theme to pull\");\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\";\nimport { themes } from \"@fluid-app/themes-api-client\";\n\nconst STATIC_ROUTES = [\n { label: \"Home\", path: \"/home\" },\n { label: \"Shop\", path: \"/home/shop\" },\n { label: \"Join / Sign Up\", path: \"/home/join\" },\n { label: \"Cart\", path: \"/cart\" },\n { label: \"Blog\", path: \"/home/blog\" },\n { label: \"Categories (all)\", path: \"/home/categories\" },\n { label: \"Collections (all)\", path: \"/home/collections\" },\n] as const;\n\nconst RESOURCE_ROUTES = [\n {\n label: \"Category\",\n type: \"category\",\n template: \"/home/categories/%s\",\n fallback: \"/home/categories\",\n },\n {\n label: \"Collection\",\n type: \"collection\",\n template: \"/home/collections/%s\",\n fallback: \"/home/collections\",\n },\n {\n label: \"Product\",\n type: \"product\",\n template: \"/home/products/%s\",\n fallback: \"/home/shop\",\n },\n {\n label: \"Library\",\n type: \"library\",\n template: \"/home/libraries/%s\",\n fallback: \"/home/libraries\",\n },\n {\n label: \"Post\",\n type: \"post\",\n template: \"/home/posts/%s\",\n fallback: \"/home/blog\",\n },\n {\n label: \"Media\",\n type: \"medium\",\n template: \"/home/media/%s\",\n fallback: \"/home/media\",\n },\n {\n label: \"Enrollment Pack\",\n type: \"enrollment_pack\",\n template: \"/home/enrollments/%s\",\n fallback: \"/home/join\",\n },\n {\n label: \"Page\",\n type: \"page\",\n template: \"/home/pages/%s\",\n fallback: \"/home/pages\",\n },\n] as const;\n\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 themes.getApplicationThemeAvailableThemeables(\n api,\n themeId,\n { themeable: dest.resourceType, per_page: 50 },\n );\n const resources = body.available_themeables ?? [];\n\n if (!resources.length) {\n console.log(`No ${dest.label} resources found, using listing page.`);\n path = dest.fallback;\n } else {\n const { 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 ?? \"Untitled\",\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":["themes.listThemeResources","themes.updateThemeResource","themes.deleteThemeResource","themes.listApplicationThemes","themes.getApplicationTheme","themes.getApplicationTheme","themes.createApplicationTheme","themes.createApplicationTheme","themes.publishApplicationTheme","themes.getApplicationThemeAvailableThemeables"],"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","../../../api-clients/themes/src/namespaces/v0.ts","../src/theme/syncer.ts","../src/theme/dev-server/index.ts","../src/theme-picker.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 /**\n * Credentials mode for fetch requests.\n * Set to `\"include\"` for cookie-based (same-origin BFF) authentication.\n * @default undefined (browser default: \"same-origin\")\n */\n credentials?: RequestCredentials;\n}\n\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 {\n baseUrl,\n getAuthToken,\n onAuthError,\n defaultHeaders = {},\n credentials,\n } = 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 if (credentials) fetchOptions.credentials = credentials;\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 (credentials) fetchOptions.credentials = credentials;\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 {\n createFetchClient,\n type FetchClient,\n} from \"@fluid-app/api-client-core\";\nimport { getAuthToken } from \"@fluid-app/fluid-cli\";\n\nexport type ApiClient = FetchClient;\n\n/** Base URL for all API calls. Set FLUID_API_BASE to route through a BFF. */\nfunction getApiBase(): string {\n return process.env[\"FLUID_API_BASE\"] ?? \"https://api.fluid.app\";\n}\n\nexport function createApiClient(tokenOverride?: string): ApiClient {\n return createFetchClient({\n baseUrl: getApiBase(),\n getAuthToken: () => tokenOverride ?? getAuthToken() ?? null,\n });\n}\n\nexport function requireToken(): string {\n const token = getAuthToken();\n if (!token) {\n console.error(\"Not logged in. Run `fluid login` first.\");\n process.exit(1);\n }\n return token;\n}\n","import { 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","/**\n * Generated API client functions for v0\n *\n * DO NOT EDIT THIS FILE DIRECTLY\n * This file is auto-generated. To update:\n * 1. Update the OpenAPI spec file\n * 2. Run: pnpm generate\n */\n\nimport type { FetchClient } from \"../lib/fetch-client\";\nimport type { operations } from \"../generated/v0\";\n\n// ============================================================================\n// applicationthemetemplates\n// ============================================================================\n\n/**\n * Lists all theme templates\n * \n *\n * @param client - Fetch client instance\n \n */\nexport async function listThemeTemplates(\n client: FetchClient,\n): Promise<\n operations[\"listThemeTemplates\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_theme_templates`);\n}\n\n/**\n * Creates a theme template\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createThemeTemplate(\n client: FetchClient,\n body: NonNullable<\n operations[\"createThemeTemplate\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createThemeTemplate\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates`, body);\n}\n\n/**\n * List all mysite themes\n * List all mysite themes\n *\n * @param client - Fetch client instance\n \n */\nexport async function listMysiteThemes(\n client: FetchClient,\n): Promise<\n operations[\"listMysiteThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_theme_templates/mysite_themes`);\n}\n\n/**\n * Retrieves a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_theme_templates/${id}`);\n}\n\n/**\n * Updates a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateThemeTemplate(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateThemeTemplate\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/application_theme_templates/${id}`, body);\n}\n\n/**\n * Deletes a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"deleteThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/application_theme_templates/${id}`);\n}\n\n/**\n * Returns all available themeables for theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeTemplateAvailableThemeables(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeTemplateAvailableThemeables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_theme_templates/${id}/available_themeables`,\n );\n}\n\n/**\n * Get available variables for a theme template\n * Get available variables that can be used in the theme template\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeTemplateAvailableVariables(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeTemplateAvailableVariables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_theme_templates/${id}/available_variables`,\n );\n}\n\n/**\n * Clones a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function cloneThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"cloneThemeTemplate\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/clone`);\n}\n\n/**\n * Publishes the template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function publishThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"publishThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/publish`);\n}\n\n/**\n * Renders a page for a theme template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function renderThemeTemplatePage(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"renderThemeTemplatePage\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"renderThemeTemplatePage\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(\n `/api/application_theme_templates/${id}/render_page`,\n body,\n );\n}\n\n/**\n * Renders a section template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function renderThemeTemplateSection(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"renderThemeTemplateSection\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/render_section`);\n}\n\n/**\n * Sets a theme template as default\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function setDefaultThemeTemplate(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"setDefaultThemeTemplate\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_theme_templates/${id}/set_default`);\n}\n\n/**\n * Updates themeable records to be used by the specified template\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function updateThemeTemplateThemeables(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"updateThemeTemplateThemeables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.put(`/api/application_theme_templates/${id}/themeables_update`);\n}\n\n// ============================================================================\n// application-themes\n// ============================================================================\n\n/**\n * List application themes\n * Get all application themes with optional filters\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listApplicationThemes(\n client: FetchClient,\n params?: operations[\"listApplicationThemes\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listApplicationThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes`, params);\n}\n\n/**\n * Create an application theme\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createApplicationTheme(\n client: FetchClient,\n body: NonNullable<\n operations[\"createApplicationTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createApplicationTheme\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes`, body);\n}\n\n/**\n * Get current active application theme\n * \n *\n * @param client - Fetch client instance\n \n */\nexport async function getActiveApplicationTheme(\n client: FetchClient,\n): Promise<\n operations[\"getActiveApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes/active`);\n}\n\n/**\n * Import an application theme from zip file\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function importApplicationThemeFromZip(\n client: FetchClient,\n body: NonNullable<\n operations[\"importApplicationThemeFromZip\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"importApplicationThemeFromZip\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/import_zip`, body);\n}\n\n/**\n * Get an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param [params] - params\n */\nexport async function getApplicationTheme(\n client: FetchClient,\n id: string | number,\n params?: operations[\"getApplicationTheme\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"getApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes/${id}`, params);\n}\n\n/**\n * Update an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateApplicationTheme(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateApplicationTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/application_themes/${id}`, body);\n}\n\n/**\n * Delete an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"deleteApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/application_themes/${id}`);\n}\n\n/**\n * Returns available themeables for a given type scoped to the theme's company\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param [params] - params\n */\nexport async function getApplicationThemeAvailableThemeables(\n client: FetchClient,\n id: string | number,\n params?: operations[\"getApplicationThemeAvailableThemeables\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"getApplicationThemeAvailableThemeables\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_themes/${id}/available_themeables`,\n params,\n );\n}\n\n/**\n * Clone an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function cloneApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"cloneApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/${id}/clone`);\n}\n\n/**\n * Import an application theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function importApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"importApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/${id}/import`);\n}\n\n/**\n * Publishes the theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function publishApplicationTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"publishApplicationTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/application_themes/${id}/publish`);\n}\n\n/**\n * Get theme assets\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeAssets(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeAssets\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/application_themes/${id}/theme_assets`);\n}\n\n// ============================================================================\n// applicationthemeresources\n// ============================================================================\n\n/**\n * Lists all theme resources\n *\n *\n * @param client - Fetch client instance\n * @param application_theme_id - application_theme_id\n */\nexport async function listThemeResources(\n client: FetchClient,\n application_theme_id: string | number,\n): Promise<\n operations[\"listThemeResources\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(\n `/api/application_themes/${application_theme_id}/resources`,\n );\n}\n\n/**\n * Updates a theme resource\n *\n *\n * @param client - Fetch client instance\n * @param application_theme_id - application_theme_id\n * @param body - body\n */\nexport async function updateThemeResource(\n client: FetchClient,\n application_theme_id: string | number,\n body: NonNullable<\n operations[\"updateThemeResource\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateThemeResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.put(\n `/api/application_themes/${application_theme_id}/resources`,\n body,\n );\n}\n\n/**\n * Deletes a theme resource\n *\n *\n * @param client - Fetch client instance\n * @param application_theme_id - application_theme_id\n * @param body - body\n */\nexport async function deleteThemeResource(\n client: FetchClient,\n application_theme_id: string | number,\n body: NonNullable<\n operations[\"deleteThemeResource\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"deleteThemeResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(\n `/api/application_themes/${application_theme_id}/resources`,\n { body },\n );\n}\n\n// ============================================================================\n// file-resources\n// ============================================================================\n\n/**\n * Returns a list of file resources\n *\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listFileResources(\n client: FetchClient,\n params?: operations[\"listFileResources\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listFileResources\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/file_resources`, params);\n}\n\n/**\n * Creates a file resource\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createFileResource(\n client: FetchClient,\n body: NonNullable<\n operations[\"createFileResource\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createFileResource\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/file_resources`, body);\n}\n\n/**\n * Creates multiple file resources\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function bulkCreateFileResources(\n client: FetchClient,\n body: NonNullable<\n operations[\"bulkCreateFileResources\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"bulkCreateFileResources\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/file_resources/bulk_create`, body);\n}\n\n/**\n * Deletes multiple file resources\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function bulkDestroyFileResources(\n client: FetchClient,\n body: NonNullable<\n operations[\"bulkDestroyFileResources\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"bulkDestroyFileResources\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/file_resources/bulk_destroy`, { body });\n}\n\n/**\n * Shows a file resource\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function showFileResource(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"showFileResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/file_resources/${id}`);\n}\n\n/**\n * Deletes a file resource\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function destroyFileResource(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"destroyFileResource\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/file_resources/${id}`);\n}\n\n// ============================================================================\n// root-themes\n// ============================================================================\n\n/**\n * List root themes\n * Get all root themes with optional filters\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listRootThemes(\n client: FetchClient,\n params?: operations[\"listRootThemes\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listRootThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/root_themes`, params);\n}\n\n/**\n * Create a root theme\n *\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createRootTheme(\n client: FetchClient,\n body: NonNullable<\n operations[\"createRootTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createRootTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/root_themes`, body);\n}\n\n/**\n * List company root themes\n * Get all company root themes with optional filters\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listCompanyRootThemes(\n client: FetchClient,\n params?: operations[\"listCompanyRootThemes\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listCompanyRootThemes\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/root_themes/my`, params);\n}\n\n/**\n * Update a root theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateRootTheme(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateRootTheme\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateRootTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/root_themes/${id}`, body);\n}\n\n/**\n * Delete a root theme\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteRootTheme(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"deleteRootTheme\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.delete(`/api/root_themes/${id}`);\n}\n\n/**\n * Update a root theme status\n *\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateRootThemeStatus(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateRootThemeStatus\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateRootThemeStatus\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/root_themes/${id}/status`, body);\n}\n\n// ============================================================================\n// theme-region-rules\n// ============================================================================\n\n/**\n * List theme region rules\n * Retrieve a list of theme region rules for the current company\n *\n * @param client - Fetch client instance\n * @param [params] - params\n */\nexport async function listThemeRegionRules(\n client: FetchClient,\n params?: operations[\"listThemeRegionRules\"][\"parameters\"][\"query\"],\n): Promise<\n operations[\"listThemeRegionRules\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/theme_region_rules`, params);\n}\n\n/**\n * Create theme region rule\n * Create a new theme region rule\n *\n * @param client - Fetch client instance\n * @param body - body\n */\nexport async function createThemeRegionRule(\n client: FetchClient,\n body: NonNullable<\n operations[\"createThemeRegionRule\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"createThemeRegionRule\"][\"responses\"][201][\"content\"][\"application/json\"]\n> {\n return client.post(`/api/theme_region_rules`, body);\n}\n\n/**\n * Show theme region rule\n * Retrieve a specific theme region rule\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function getThemeRegionRule(\n client: FetchClient,\n id: string | number,\n): Promise<\n operations[\"getThemeRegionRule\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.get(`/api/theme_region_rules/${id}`);\n}\n\n/**\n * Update theme region rule\n * Update an existing theme region rule\n *\n * @param client - Fetch client instance\n * @param id - id\n * @param body - body\n */\nexport async function updateThemeRegionRule(\n client: FetchClient,\n id: string | number,\n body: NonNullable<\n operations[\"updateThemeRegionRule\"][\"requestBody\"]\n >[\"content\"][\"application/json\"],\n): Promise<\n operations[\"updateThemeRegionRule\"][\"responses\"][200][\"content\"][\"application/json\"]\n> {\n return client.patch(`/api/theme_region_rules/${id}`, body);\n}\n\n/**\n * Delete theme region rule\n * Delete a theme region rule\n *\n * @param client - Fetch client instance\n * @param id - id\n */\nexport async function deleteThemeRegionRule(\n client: FetchClient,\n id: string | number,\n): Promise<void> {\n return client.delete(`/api/theme_region_rules/${id}`);\n}\n","import { sep } from \"node:path\";\nimport type { ApiClient } from \"../api.js\";\nimport type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\nimport { themes, type components } from \"@fluid-app/themes-api-client\";\n\ntype RemoteResource = components[\"schemas\"][\"ApplicationThemeResource\"];\n\nexport interface SyncResult {\n uploaded: number;\n downloaded: number;\n deleted: number;\n errors: string[];\n}\n\nexport class Syncer {\n private checksums = new Map<string, string>();\n\n constructor(\n private api: ApiClient,\n private themeId: number,\n private themeRoot: ThemeRoot,\n ) {}\n\n // ─── Checksum Management ──────────────────────────────────────────────────\n\n async fetchChecksums(): Promise<void> {\n const body = await themes.listThemeResources(this.api, this.themeId);\n this.updateChecksums(body.application_theme_resources ?? []);\n }\n\n private updateChecksums(resources: RemoteResource[]): void {\n for (const r of resources) {\n if (r.key && r.checksum) this.checksums.set(r.key, r.checksum);\n }\n for (const key of this.checksums.keys()) {\n if (this.checksums.has(`${key}.liquid`)) this.checksums.delete(key);\n }\n }\n\n hasChanged(file: ThemeFile): boolean {\n return file.checksum() !== this.checksums.get(file.relativePath);\n }\n\n remoteKeys(): string[] {\n return [...this.checksums.keys()];\n }\n\n // ─── Upload ───────────────────────────────────────────────────────────────\n\n async uploadFile(file: ThemeFile): Promise<void> {\n if (file.isText) {\n await themes.updateThemeResource(this.api, this.themeId, {\n application_theme_resource: {\n key: file.relativePath,\n content: file.read(),\n },\n });\n } else {\n await this.uploadBinaryFile(file);\n }\n }\n\n private async uploadBinaryFile(file: ThemeFile): Promise<void> {\n // Step 1: Create DAM placeholder\n const placeholderBody = await this.api.post<{\n asset: { id: number; canonical_path: string };\n }>(\"/api/dam/assets\", {\n placeholder_asset: {\n description: `Uploaded via Fluid CLI: ${file.name}`,\n mime_type: file.mime.name,\n name: file.name,\n },\n });\n const asset = placeholderBody.asset;\n\n // Step 2: Get ImageKit auth token\n const authBody = await this.api.post<{\n token: string;\n signature: string;\n expire: number;\n }>(\"/api/dam/assets/imagekit_auth\", {});\n\n // Step 3: Upload to ImageKit via multipart\n const folder = this.canonicalPathToImageKitFolder(asset.canonical_path);\n const formData = new FormData();\n const blob = new Blob([file.readBinary() as unknown as ArrayBuffer], {\n type: file.mime.name,\n });\n formData.append(\"file\", blob, file.name);\n formData.append(\"token\", authBody.token);\n formData.append(\"signature\", authBody.signature);\n formData.append(\"expire\", String(authBody.expire));\n formData.append(\"folder\", folder);\n formData.append(\"fileName\", file.name);\n formData.append(\"publicKey\", \"public_j7s4Ih9ETh/OCp41mVQH7tlXBdU=\");\n\n const ikResp = await fetch(\n \"https://upload.imagekit.io/api/v1/files/upload\",\n {\n method: \"POST\",\n body: formData,\n },\n );\n if (!ikResp.ok) throw new Error(`ImageKit upload failed: ${ikResp.status}`);\n const ikBody = (await ikResp.json()) as {\n fileId: string;\n url: string;\n thumbnailUrl: string;\n size: number;\n height?: number;\n width?: number;\n };\n\n // Step 4: Backfill DAM asset\n const backfillPayload: Record<string, unknown> = {\n asset: {\n id: asset.id,\n imagekit_file_id: ikBody.fileId,\n imagekit_url: ikBody.url,\n mime_type: file.mime.name,\n name: file.name,\n file_size: ikBody.size,\n expected_path: asset.canonical_path,\n },\n };\n if (ikBody.height)\n (backfillPayload[\"asset\"] as Record<string, unknown>)[\"height\"] =\n ikBody.height;\n if (ikBody.width)\n (backfillPayload[\"asset\"] as Record<string, unknown>)[\"width\"] =\n ikBody.width;\n\n const backfillBody = await this.api.post<{\n asset: { code: string; default_variant_url: string };\n }>(\"/api/dam/assets/backfill_imagekit\", backfillPayload);\n\n // Step 5: Associate with theme resource\n await themes.updateThemeResource(this.api, this.themeId, {\n application_theme_resource: {\n key: file.relativePath,\n dam_asset: {\n dam_asset_code: backfillBody.asset.code,\n content_type: file.mime.name,\n content_size: ikBody.size,\n filename: file.name,\n handle: backfillBody.asset.code,\n url: backfillBody.asset.default_variant_url,\n preview_image_url: ikBody.thumbnailUrl,\n },\n },\n });\n }\n\n private canonicalPathToImageKitFolder(canonicalPath: string): string {\n const parts = canonicalPath.split(\".\");\n const companyId = parts[0] ?? \"unknown\";\n const category = parts[1] ?? \"files\";\n const assetCode = parts[2] ?? \"unknown\";\n const folderMap: Record<string, string> = {\n images: \"images\",\n videos: \"videos\",\n audio: \"audio\",\n documents: \"documents\",\n files: \"files\",\n };\n return `${companyId}/${folderMap[category] ?? \"files\"}/${assetCode}`;\n }\n\n // ─── Delete ───────────────────────────────────────────────────────────────\n\n async deleteRemoteFile(relativePath: string): Promise<void> {\n await themes.deleteThemeResource(this.api, this.themeId, {\n application_theme_resource: { key: relativePath },\n });\n this.checksums.delete(relativePath);\n }\n\n // ─── Download ─────────────────────────────────────────────────────────────\n\n async downloadAll(): Promise<RemoteResource[]> {\n const body = await themes.listThemeResources(this.api, this.themeId);\n const resources = body.application_theme_resources ?? [];\n this.updateChecksums(resources);\n return resources;\n }\n\n async downloadBinaryAsset(url: string): Promise<Buffer> {\n const resp = await fetch(url);\n if (!resp.ok) throw new Error(`Failed to download asset: ${resp.status}`);\n return Buffer.from(await resp.arrayBuffer());\n }\n\n // ─── Full Upload ──────────────────────────────────────────────────────────\n\n async uploadTheme(\n opts: {\n delete?: boolean;\n 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 (\n resource.content !== undefined &&\n resource.content !== null\n ) {\n const content =\n typeof resource.content === \"string\"\n ? resource.content\n : JSON.stringify(resource.content);\n file.write(content);\n }\n result.downloaded++;\n } catch (e) {\n result.errors.push(`Download ${resource.key}: ${e}`);\n }\n opts.onProgress?.(++done, resources.length);\n }\n\n if (opts.delete) {\n const remoteKeys = new Set(resources.map((r) => r.key));\n for (const file of this.themeRoot.files()) {\n if (!remoteKeys.has(file.relativePath)) {\n try {\n const { unlinkSync } = await import(\"node:fs\");\n unlinkSync(file.absolutePath);\n result.deleted++;\n } catch {\n // ignore\n }\n }\n }\n }\n\n return result;\n }\n}\n","import http from \"node:http\";\nimport { SSEStream } from \"./sse.js\";\nimport { proxyRequest } from \"./proxy.js\";\nimport { watchTheme } from \"./watcher.js\";\nimport { Syncer } from \"../syncer.js\";\nimport type { ThemeRoot } from \"../root.js\";\nimport type { ApiClient } from \"../../api.js\";\n\nexport interface DevServerOptions {\n host: string;\n port: number;\n reloadMode: \"full-page\" | \"off\";\n}\n\nexport interface DevServerTheme {\n id: number;\n name: string;\n company: string;\n editorUrl?: string;\n}\n\nexport async function startDevServer(\n api: ApiClient,\n theme: DevServerTheme,\n themeRoot: ThemeRoot,\n opts: DevServerOptions,\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 chalk from \"chalk\";\nimport prompts from \"prompts\";\nimport type { createApiClient } from \"./api.js\";\nimport { themes, type components } from \"@fluid-app/themes-api-client\";\n\nexport type ApplicationTheme = components[\"schemas\"][\"ApplicationTheme\"];\n\nconst PAGE_SIZE = 50;\nconst LOAD_MORE_VALUE = -1;\n\nfunction themeLabel(t: ApplicationTheme): string {\n const active = t.status === \"active\" ? ` ${chalk.green(\"[active]\")}` : \"\";\n return `${t.name} (#${t.id})${active}`;\n}\n\nfunction themeChoices(\n themeList: ApplicationTheme[],\n hasMore: boolean,\n): prompts.Choice[] {\n const choices: prompts.Choice[] = themeList.map((t) => ({\n title: themeLabel(t),\n value: t.id,\n }));\n if (hasMore) {\n choices.push({\n title: chalk.dim(`── Load more themes ──`),\n value: LOAD_MORE_VALUE,\n });\n }\n return choices;\n}\n\nasync function fetchThemesPage(\n api: ReturnType<typeof createApiClient>,\n page: number,\n searchQuery?: string,\n): Promise<{\n themes: ApplicationTheme[];\n hasMore: boolean;\n}> {\n const body = await themes.listApplicationThemes(api, {\n per_page: PAGE_SIZE,\n page,\n ...(searchQuery ? { search_query: searchQuery } : {}),\n });\n const list = body.application_themes ?? [];\n const totalPages = body.meta?.total_pages ?? 1;\n return { themes: list, hasMore: page < totalPages };\n}\n\nexport async function selectTheme(\n api: ReturnType<typeof createApiClient>,\n message: string,\n): Promise<ApplicationTheme> {\n const allThemes: ApplicationTheme[] = [];\n let page = 1;\n let hasMore = true;\n let initialIndex = 0;\n\n // Search cache — persists across suggest calls\n let searchQuery = \"\";\n let searchResults: ApplicationTheme[] = [];\n\n while (true) {\n if (hasMore && allThemes.length < page * PAGE_SIZE) {\n const result = await fetchThemesPage(api, page);\n allThemes.push(...result.themes);\n hasMore = result.hasMore;\n }\n\n if (!allThemes.length) {\n console.error(\"No themes found.\");\n process.exit(1);\n }\n\n const choices = themeChoices(allThemes, hasMore);\n\n const { id } = await prompts(\n {\n type: \"autocomplete\",\n name: \"id\",\n message,\n initial: initialIndex,\n choices,\n suggest: async (input: string, choices: prompts.Choice[]) => {\n if (!input) {\n searchQuery = \"\";\n searchResults = [];\n return choices;\n }\n\n if (input !== searchQuery) {\n searchQuery = input;\n try {\n const result = await fetchThemesPage(api, 1, input);\n searchResults = result.themes;\n } catch {\n searchResults = [];\n }\n }\n\n return searchResults.map((t) => ({\n title: themeLabel(t),\n value: t.id,\n }));\n },\n },\n { onCancel: () => process.exit(130) },\n );\n\n if (id === LOAD_MORE_VALUE) {\n initialIndex = allThemes.length;\n page++;\n continue;\n }\n\n if (!id) {\n console.error(\"No theme selected.\");\n process.exit(1);\n }\n\n // Check loaded themes first, then search results\n const found =\n allThemes.find((t) => t.id === id) ??\n searchResults.find((t) => t.id === id);\n if (found) return found;\n\n // Fetch directly by ID as fallback\n const body = await themes.getApplicationTheme(api, id);\n return body.application_theme;\n }\n}\n\nexport async function findTheme(\n api: ReturnType<typeof createApiClient>,\n identifier: string,\n): Promise<ApplicationTheme> {\n // Try ID lookup first\n const idNum = Number(identifier);\n if (Number.isInteger(idNum) && idNum > 0) {\n try {\n const body = await themes.getApplicationTheme(api, idNum);\n if (body.application_theme) return body.application_theme;\n } catch {\n // Not found by ID, fall through to search\n }\n }\n\n // Search by name via API with pagination\n let page = 1;\n let hasMore = true;\n while (hasMore) {\n const result = await fetchThemesPage(api, page, identifier);\n const found = result.themes.find(\n (t) => t.name.toLowerCase() === identifier.toLowerCase(),\n );\n if (found) return found;\n hasMore = result.hasMore;\n page++;\n }\n\n console.error(`No theme found with identifier: ${identifier}`);\n process.exit(1);\n}\n","import { 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\";\nimport { themes } from \"@fluid-app/themes-api-client\";\nimport { findTheme, type ApplicationTheme } from \"../theme-picker.js\";\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 return findTheme(api, identifier);\n }\n\n // Reuse stored dev theme if it still exists\n const { devThemeId } = getPluginState();\n if (devThemeId) {\n try {\n const body = await themes.getApplicationTheme(api, devThemeId);\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 themes.createApplicationTheme(api, {\n application_theme: { name, status: \"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 const editorUrl = `https://admin.fluid.app/themes/${theme.id}/editor`;\n\n let stop: (() => void) | undefined;\n\n const cleanup = () => {\n stop?.();\n process.exit(0);\n };\n process.on(\"SIGINT\", cleanup);\n process.on(\"SIGTERM\", cleanup);\n\n stop = await startDevServer(\n api,\n {\n id: theme.id,\n name: theme.name,\n company,\n editorUrl,\n },\n themeRoot,\n { host: opts.host, port, reloadMode },\n (address) => {\n console.log(`\\n Dev server: ${address}`);\n console.log(` Web editor: ${editorUrl}`);\n console.log(\"\\n Watching for file changes…\\n\");\n\n if (opts.navigate) {\n import(\"open\").then((m) => m.default(`${address}/home`));\n }\n },\n );\n\n // Keep process alive\n await new Promise(() => {});\n },\n );\n}\n","import { 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\";\nimport { themes } from \"@fluid-app/themes-api-client\";\nimport {\n selectTheme,\n findTheme,\n type ApplicationTheme,\n} from \"../theme-picker.js\";\n\nexport function createPushCommand(): Command {\n return new Command(\"push\")\n .description(\"Push local theme files to a remote theme\")\n .option(\"-t, --theme <name-or-id>\", \"Theme name or ID to push to\")\n .option(\"-n, --nodelete\", \"Do not delete remote files missing locally\")\n .option(\"-f, --force\", \"Skip schema validation\")\n .option(\"-p, --publish\", \"Publish the theme after pushing\")\n .option(\n \"-u, --unpublished\",\n \"Create a new unpublished theme and push to it\",\n )\n .option(\"--root <path>\", \"Theme root directory\", \".\")\n .action(\n async (opts: {\n theme?: string;\n nodelete?: boolean;\n force?: boolean;\n publish?: boolean;\n unpublished?: boolean;\n root: string;\n }) => {\n requireToken();\n\n 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 let theme: ApplicationTheme;\n\n if (opts.unpublished) {\n const { name } = await prompts(\n {\n type: \"text\",\n name: \"name\",\n message: \"Name for the new theme\",\n },\n { onCancel: () => process.exit(130) },\n );\n if (!name) {\n console.error(\"Theme name is required.\");\n process.exit(1);\n }\n const body = await themes.createApplicationTheme(api, {\n application_theme: { name, status: \"draft\" },\n });\n theme = body.application_theme;\n console.log(\n `Created unpublished theme: ${theme.name} (#${theme.id})`,\n );\n } else {\n theme = opts.theme\n ? await findTheme(api, opts.theme)\n : await selectTheme(api, \"Select a theme to push to\");\n }\n\n const syncer = new Syncer(api, theme.id, themeRoot);\n const spinner = ora(`Pushing to ${theme.name} (#${theme.id})…`).start();\n\n const result = await syncer.uploadTheme({\n delete: !opts.nodelete,\n 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 themes.publishApplicationTheme(api, theme.id);\n pubSpinner.succeed(\"Theme published.\");\n } catch (e) {\n pubSpinner.fail(`Publish failed: ${e}`);\n }\n }\n },\n );\n}\n","import { Command } from \"commander\";\nimport ora from \"ora\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { Syncer } from \"../theme/syncer.js\";\nimport { selectTheme, findTheme } from \"../theme-picker.js\";\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 = opts.theme\n ? await findTheme(api, opts.theme)\n : await selectTheme(api, \"Select a theme to pull\");\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\";\nimport { themes } from \"@fluid-app/themes-api-client\";\n\nfunction localSuggest(\n input: string,\n choices: prompts.Choice[],\n): prompts.Choice[] {\n if (!input) return choices;\n const lower = input.toLowerCase();\n return choices.filter((c) => c.title.toLowerCase().includes(lower));\n}\n\ninterface ThemeTemplate {\n id: number;\n name: string;\n themeable_type: string;\n default: boolean;\n}\n\ninterface TemplatesResponse {\n templates: ThemeTemplate[];\n}\n\nconst THEMEABLE_TYPE_MAP: Record<string, string> = {\n \"/home\": \"home_page\",\n \"/home/shop\": \"shop_page\",\n \"/home/join\": \"join_page\",\n \"/cart\": \"cart_page\",\n \"/home/blog\": \"post_page\",\n \"/home/categories\": \"category_page\",\n \"/home/collections\": \"collection_page\",\n};\n\nconst STATIC_ROUTES = [\n { label: \"Home\", path: \"/home\" },\n { label: \"Shop\", path: \"/home/shop\" },\n { label: \"Join / Sign Up\", path: \"/home/join\" },\n { label: \"Cart\", path: \"/cart\" },\n { label: \"Blog\", path: \"/home/blog\" },\n { label: \"Categories (all)\", path: \"/home/categories\" },\n { label: \"Collections (all)\", path: \"/home/collections\" },\n] as const;\n\nconst RESOURCE_ROUTES = [\n {\n label: \"Category\",\n type: \"category\",\n template: \"/home/categories/%s\",\n fallback: \"/home/categories\",\n },\n {\n label: \"Collection\",\n type: \"collection\",\n template: \"/home/collections/%s\",\n fallback: \"/home/collections\",\n },\n {\n label: \"Product\",\n type: \"product\",\n template: \"/home/products/%s\",\n fallback: \"/home/shop\",\n },\n {\n label: \"Library\",\n type: \"library\",\n template: \"/home/libraries/%s\",\n fallback: \"/home/libraries\",\n },\n {\n label: \"Post\",\n type: \"post\",\n template: \"/home/posts/%s\",\n fallback: \"/home/blog\",\n },\n {\n label: \"Media\",\n type: \"medium\",\n template: \"/home/media/%s\",\n fallback: \"/home/media\",\n },\n {\n label: \"Enrollment Pack\",\n type: \"enrollment_pack\",\n template: \"/home/enrollments/%s\",\n fallback: \"/home/join\",\n },\n {\n label: \"Page\",\n type: \"page\",\n template: \"/home/pages/%s\",\n fallback: \"/home/pages\",\n },\n] as const;\n\nasync function fetchTemplatesForType(\n api: ReturnType<typeof createApiClient>,\n themeId: number,\n themeableType: string,\n): Promise<ThemeTemplate[]> {\n const params = new URLSearchParams({\n application_theme_id: String(themeId),\n themeable_type: themeableType,\n published: \"true\",\n });\n const body = await api.get<TemplatesResponse>(\n `/api/application_theme_templates?${params}`,\n );\n return body.templates ?? [];\n}\n\nasync function selectTemplate(\n api: ReturnType<typeof createApiClient>,\n themeId: number,\n themeableType: string,\n onCancel: () => void,\n): Promise<number | null> {\n const templates = await fetchTemplatesForType(api, themeId, themeableType);\n if (templates.length <= 1) return null;\n\n const templateChoices = templates.map((t) => ({\n title: `${t.name}${t.default ? \" (default)\" : \"\"}`,\n value: t.id,\n }));\n const { templateId } = await prompts(\n {\n type: \"autocomplete\",\n name: \"templateId\",\n message: \"Select a template\",\n choices: templateChoices,\n suggest: (input: string, choices: prompts.Choice[]) =>\n Promise.resolve(localSuggest(input, choices)),\n },\n { onCancel },\n );\n\n return templateId ?? null;\n}\n\nexport function createNavigateCommand(): Command {\n return new Command(\"navigate\")\n .description(\"Interactively navigate to a route in the dev server browser\")\n .option(\"--host <host>\", \"Dev server host\", \"127.0.0.1\")\n .option(\"--port <port>\", \"Dev server port\", \"9292\")\n .option(\"-t, --theme <id>\", \"Theme ID (defaults to active dev theme)\")\n .action(async (opts: { host: string; port: string; theme?: string }) => {\n requireToken();\n\n const themeId = opts.theme\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: \"autocomplete\",\n name: \"dest\",\n message: \"Select a route\",\n choices,\n suggest: (input: string, choices: prompts.Choice[]) =>\n Promise.resolve(localSuggest(input, choices)),\n },\n { onCancel },\n );\n\n if (!dest) return;\n\n const api = createApiClient();\n let path: string;\n let themeableType: string | undefined;\n\n if (typeof dest === \"string\") {\n path = dest;\n themeableType = THEMEABLE_TYPE_MAP[dest];\n } else {\n themeableType = dest.resourceType;\n const body = await themes.getApplicationThemeAvailableThemeables(\n api,\n themeId,\n { themeable: dest.resourceType, per_page: 50 },\n );\n const resources = body.available_themeables ?? [];\n\n if (!resources.length) {\n console.log(`No ${dest.label} resources found, using listing page.`);\n path = dest.fallback;\n } else {\n const resourceChoices = resources.map((r) => ({\n title: r.title ?? r.slug ?? \"Untitled\",\n value: r.slug,\n }));\n const { slug } = await prompts(\n {\n type: \"autocomplete\",\n name: \"slug\",\n message: `Select a ${dest.label.toLowerCase()}`,\n choices: resourceChoices,\n suggest: (input: string, choices: prompts.Choice[]) =>\n Promise.resolve(localSuggest(input, choices)),\n },\n { onCancel },\n );\n path = dest.template.replace(\"%s\", slug as string);\n }\n }\n\n let templateParam = \"\";\n if (themeableType) {\n const templateId = await selectTemplate(\n api,\n themeId,\n themeableType,\n onCancel,\n );\n if (templateId) {\n templateParam = `?theme_template_id=${templateId}`;\n }\n }\n\n const url = `${address}${path}${templateParam}`;\n console.log(`\\nNavigating to: ${url}\\n`);\n const open = (await import(\"open\")).default;\n await open(url);\n });\n}\n","import { Command } from \"commander\";\nimport type { PluginContext } from \"@fluid-app/fluid-cli\";\nimport { createDevCommand } from \"./dev.js\";\nimport { createPushCommand } from \"./push.js\";\nimport { createPullCommand } from \"./pull.js\";\nimport { 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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluid-app/fluid-cli-theme-dev",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"open": "^10.0.0",
|
|
24
24
|
"ora": "^8.0.0",
|
|
25
25
|
"prompts": "^2.4.2",
|
|
26
|
-
"@fluid-app/fluid-cli": "0.1.
|
|
26
|
+
"@fluid-app/fluid-cli": "0.1.6"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^24",
|
package/src/commands/dev.ts
CHANGED
|
@@ -104,6 +104,7 @@ export function createDevCommand(): Command {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const theme = await ensureDevTheme(api, opts.theme);
|
|
107
|
+
const editorUrl = `https://admin.fluid.app/themes/${theme.id}/editor`;
|
|
107
108
|
|
|
108
109
|
let stop: (() => void) | undefined;
|
|
109
110
|
|
|
@@ -120,14 +121,13 @@ export function createDevCommand(): Command {
|
|
|
120
121
|
id: theme.id,
|
|
121
122
|
name: theme.name,
|
|
122
123
|
company,
|
|
123
|
-
editorUrl
|
|
124
|
+
editorUrl,
|
|
124
125
|
},
|
|
125
126
|
themeRoot,
|
|
126
127
|
{ host: opts.host, port, reloadMode },
|
|
127
128
|
(address) => {
|
|
128
129
|
console.log(`\n Dev server: ${address}`);
|
|
129
|
-
|
|
130
|
-
console.log(` Web editor: ${theme.editor_url}`);
|
|
130
|
+
console.log(` Web editor: ${editorUrl}`);
|
|
131
131
|
console.log("\n Watching for file changes…\n");
|
|
132
132
|
|
|
133
133
|
if (opts.navigate) {
|
package/src/commands/navigate.ts
CHANGED
|
@@ -4,6 +4,36 @@ import { requireToken, createApiClient } from "../api.js";
|
|
|
4
4
|
import { getPluginState } from "../plugin-state.js";
|
|
5
5
|
import { themes } from "@fluid-app/themes-api-client";
|
|
6
6
|
|
|
7
|
+
function localSuggest(
|
|
8
|
+
input: string,
|
|
9
|
+
choices: prompts.Choice[],
|
|
10
|
+
): prompts.Choice[] {
|
|
11
|
+
if (!input) return choices;
|
|
12
|
+
const lower = input.toLowerCase();
|
|
13
|
+
return choices.filter((c) => c.title.toLowerCase().includes(lower));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ThemeTemplate {
|
|
17
|
+
id: number;
|
|
18
|
+
name: string;
|
|
19
|
+
themeable_type: string;
|
|
20
|
+
default: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface TemplatesResponse {
|
|
24
|
+
templates: ThemeTemplate[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const THEMEABLE_TYPE_MAP: Record<string, string> = {
|
|
28
|
+
"/home": "home_page",
|
|
29
|
+
"/home/shop": "shop_page",
|
|
30
|
+
"/home/join": "join_page",
|
|
31
|
+
"/cart": "cart_page",
|
|
32
|
+
"/home/blog": "post_page",
|
|
33
|
+
"/home/categories": "category_page",
|
|
34
|
+
"/home/collections": "collection_page",
|
|
35
|
+
};
|
|
36
|
+
|
|
7
37
|
const STATIC_ROUTES = [
|
|
8
38
|
{ label: "Home", path: "/home" },
|
|
9
39
|
{ label: "Shop", path: "/home/shop" },
|
|
@@ -65,6 +95,50 @@ const RESOURCE_ROUTES = [
|
|
|
65
95
|
},
|
|
66
96
|
] as const;
|
|
67
97
|
|
|
98
|
+
async function fetchTemplatesForType(
|
|
99
|
+
api: ReturnType<typeof createApiClient>,
|
|
100
|
+
themeId: number,
|
|
101
|
+
themeableType: string,
|
|
102
|
+
): Promise<ThemeTemplate[]> {
|
|
103
|
+
const params = new URLSearchParams({
|
|
104
|
+
application_theme_id: String(themeId),
|
|
105
|
+
themeable_type: themeableType,
|
|
106
|
+
published: "true",
|
|
107
|
+
});
|
|
108
|
+
const body = await api.get<TemplatesResponse>(
|
|
109
|
+
`/api/application_theme_templates?${params}`,
|
|
110
|
+
);
|
|
111
|
+
return body.templates ?? [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function selectTemplate(
|
|
115
|
+
api: ReturnType<typeof createApiClient>,
|
|
116
|
+
themeId: number,
|
|
117
|
+
themeableType: string,
|
|
118
|
+
onCancel: () => void,
|
|
119
|
+
): Promise<number | null> {
|
|
120
|
+
const templates = await fetchTemplatesForType(api, themeId, themeableType);
|
|
121
|
+
if (templates.length <= 1) return null;
|
|
122
|
+
|
|
123
|
+
const templateChoices = templates.map((t) => ({
|
|
124
|
+
title: `${t.name}${t.default ? " (default)" : ""}`,
|
|
125
|
+
value: t.id,
|
|
126
|
+
}));
|
|
127
|
+
const { templateId } = await prompts(
|
|
128
|
+
{
|
|
129
|
+
type: "autocomplete",
|
|
130
|
+
name: "templateId",
|
|
131
|
+
message: "Select a template",
|
|
132
|
+
choices: templateChoices,
|
|
133
|
+
suggest: (input: string, choices: prompts.Choice[]) =>
|
|
134
|
+
Promise.resolve(localSuggest(input, choices)),
|
|
135
|
+
},
|
|
136
|
+
{ onCancel },
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return templateId ?? null;
|
|
140
|
+
}
|
|
141
|
+
|
|
68
142
|
export function createNavigateCommand(): Command {
|
|
69
143
|
return new Command("navigate")
|
|
70
144
|
.description("Interactively navigate to a route in the dev server browser")
|
|
@@ -115,21 +189,27 @@ export function createNavigateCommand(): Command {
|
|
|
115
189
|
|
|
116
190
|
const { dest } = await prompts(
|
|
117
191
|
{
|
|
118
|
-
type: "
|
|
192
|
+
type: "autocomplete",
|
|
119
193
|
name: "dest",
|
|
120
194
|
message: "Select a route",
|
|
121
195
|
choices,
|
|
196
|
+
suggest: (input: string, choices: prompts.Choice[]) =>
|
|
197
|
+
Promise.resolve(localSuggest(input, choices)),
|
|
122
198
|
},
|
|
123
199
|
{ onCancel },
|
|
124
200
|
);
|
|
125
201
|
|
|
126
202
|
if (!dest) return;
|
|
127
203
|
|
|
204
|
+
const api = createApiClient();
|
|
128
205
|
let path: string;
|
|
206
|
+
let themeableType: string | undefined;
|
|
207
|
+
|
|
129
208
|
if (typeof dest === "string") {
|
|
130
209
|
path = dest;
|
|
210
|
+
themeableType = THEMEABLE_TYPE_MAP[dest];
|
|
131
211
|
} else {
|
|
132
|
-
|
|
212
|
+
themeableType = dest.resourceType;
|
|
133
213
|
const body = await themes.getApplicationThemeAvailableThemeables(
|
|
134
214
|
api,
|
|
135
215
|
themeId,
|
|
@@ -141,15 +221,18 @@ export function createNavigateCommand(): Command {
|
|
|
141
221
|
console.log(`No ${dest.label} resources found, using listing page.`);
|
|
142
222
|
path = dest.fallback;
|
|
143
223
|
} else {
|
|
224
|
+
const resourceChoices = resources.map((r) => ({
|
|
225
|
+
title: r.title ?? r.slug ?? "Untitled",
|
|
226
|
+
value: r.slug,
|
|
227
|
+
}));
|
|
144
228
|
const { slug } = await prompts(
|
|
145
229
|
{
|
|
146
|
-
type: "
|
|
230
|
+
type: "autocomplete",
|
|
147
231
|
name: "slug",
|
|
148
232
|
message: `Select a ${dest.label.toLowerCase()}`,
|
|
149
|
-
choices:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
})),
|
|
233
|
+
choices: resourceChoices,
|
|
234
|
+
suggest: (input: string, choices: prompts.Choice[]) =>
|
|
235
|
+
Promise.resolve(localSuggest(input, choices)),
|
|
153
236
|
},
|
|
154
237
|
{ onCancel },
|
|
155
238
|
);
|
|
@@ -157,7 +240,20 @@ export function createNavigateCommand(): Command {
|
|
|
157
240
|
}
|
|
158
241
|
}
|
|
159
242
|
|
|
160
|
-
|
|
243
|
+
let templateParam = "";
|
|
244
|
+
if (themeableType) {
|
|
245
|
+
const templateId = await selectTemplate(
|
|
246
|
+
api,
|
|
247
|
+
themeId,
|
|
248
|
+
themeableType,
|
|
249
|
+
onCancel,
|
|
250
|
+
);
|
|
251
|
+
if (templateId) {
|
|
252
|
+
templateParam = `?theme_template_id=${templateId}`;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const url = `${address}${path}${templateParam}`;
|
|
161
257
|
console.log(`\nNavigating to: ${url}\n`);
|
|
162
258
|
const open = (await import("open")).default;
|
|
163
259
|
await open(url);
|