@fluid-app/fluid-cli-theme-dev 0.1.3 β†’ 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @fluid-app/fluid-cli-theme-dev@0.1.3 build /home/runner/_work/fluid-mono/fluid-mono/packages/cli/theme-dev
2
+ > @fluid-app/fluid-cli-theme-dev@0.1.4 build /home/runner/_work/fluid-mono/fluid-mono/packages/cli/theme-dev
3
3
  > tsdown
4
4
 
5
5
  β„Ή tsdown v0.21.0 powered by rolldown v1.0.0-rc.7
@@ -8,11 +8,9 @@
8
8
  β„Ή target: node18
9
9
  β„Ή tsconfig: tsconfig.json
10
10
  β„Ή Build start
11
- β„Ή dist/index.mjs 39.25 kB β”‚ gzip: 11.30 kB
12
- β„Ή dist/index.mjs.map 87.52 kB β”‚ gzip: 21.90 kB
13
- β„Ή dist/index.d.mts.map  0.11 kB β”‚ gzip: 0.12 kB
14
- β„Ή dist/index.d.mts  0.19 kB β”‚ gzip: 0.16 kB
15
- β„Ή 4 files, total: 127.07 kB
16
- [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
17
-
18
- βœ” Build complete in 3302ms
11
+ β„Ή dist/index.mjs  41.47 kB β”‚ gzip: 11.70 kB
12
+ β„Ή dist/index.mjs.map 110.82 kB β”‚ gzip: 24.52 kB
13
+ β„Ή dist/index.d.mts.map  0.11 kB β”‚ gzip: 0.12 kB
14
+ β„Ή dist/index.d.mts  0.19 kB β”‚ gzip: 0.16 kB
15
+ β„Ή 4 files, total: 152.60 kB
16
+ βœ” Build complete in 1219ms
package/dist/index.mjs CHANGED
@@ -610,6 +610,92 @@ function watchTheme(root, handler) {
610
610
  return () => watcher.close();
611
611
  }
612
612
  //#endregion
613
+ //#region ../../api-clients/themes/src/namespaces/v0.ts
614
+ /**
615
+ * List application themes
616
+ * Get all application themes with optional filters
617
+ *
618
+ * @param client - Fetch client instance
619
+ * @param [params] - params
620
+ */
621
+ async function listApplicationThemes(client, params) {
622
+ return client.get(`/api/application_themes`, params);
623
+ }
624
+ /**
625
+ * Create an application theme
626
+ *
627
+ *
628
+ * @param client - Fetch client instance
629
+ * @param body - body
630
+ */
631
+ async function createApplicationTheme(client, body) {
632
+ return client.post(`/api/application_themes`, body);
633
+ }
634
+ /**
635
+ * Get an application theme
636
+ *
637
+ *
638
+ * @param client - Fetch client instance
639
+ * @param id - id
640
+ * @param [params] - params
641
+ */
642
+ async function getApplicationTheme(client, id, params) {
643
+ return client.get(`/api/application_themes/${id}`, params);
644
+ }
645
+ /**
646
+ * Returns available themeables for a given type scoped to the theme's company
647
+ *
648
+ *
649
+ * @param client - Fetch client instance
650
+ * @param id - id
651
+ * @param [params] - params
652
+ */
653
+ async function getApplicationThemeAvailableThemeables(client, id, params) {
654
+ return client.get(`/api/application_themes/${id}/available_themeables`, params);
655
+ }
656
+ /**
657
+ * Publishes the theme
658
+ *
659
+ *
660
+ * @param client - Fetch client instance
661
+ * @param id - id
662
+ */
663
+ async function publishApplicationTheme(client, id) {
664
+ return client.post(`/api/application_themes/${id}/publish`);
665
+ }
666
+ /**
667
+ * Lists all theme resources
668
+ *
669
+ *
670
+ * @param client - Fetch client instance
671
+ * @param application_theme_id - application_theme_id
672
+ */
673
+ async function listThemeResources(client, application_theme_id) {
674
+ return client.get(`/api/application_themes/${application_theme_id}/resources`);
675
+ }
676
+ /**
677
+ * Updates a theme resource
678
+ *
679
+ *
680
+ * @param client - Fetch client instance
681
+ * @param application_theme_id - application_theme_id
682
+ * @param body - body
683
+ */
684
+ async function updateThemeResource(client, application_theme_id, body) {
685
+ return client.put(`/api/application_themes/${application_theme_id}/resources`, body);
686
+ }
687
+ /**
688
+ * Deletes a theme resource
689
+ *
690
+ *
691
+ * @param client - Fetch client instance
692
+ * @param application_theme_id - application_theme_id
693
+ * @param body - body
694
+ */
695
+ async function deleteThemeResource(client, application_theme_id, body) {
696
+ return client.delete(`/api/application_themes/${application_theme_id}/resources`, { body });
697
+ }
698
+ //#endregion
613
699
  //#region src/theme/syncer.ts
614
700
  var Syncer = class {
615
701
  checksums = /* @__PURE__ */ new Map();
@@ -619,11 +705,11 @@ var Syncer = class {
619
705
  this.themeRoot = themeRoot;
620
706
  }
621
707
  async fetchChecksums() {
622
- const body = await this.api.get(`/api/application_themes/${this.themeId}/resources`);
708
+ const body = await listThemeResources(this.api, this.themeId);
623
709
  this.updateChecksums(body.application_theme_resources ?? []);
624
710
  }
625
711
  updateChecksums(resources) {
626
- for (const r of resources) if (r.key) this.checksums.set(r.key, r.checksum);
712
+ for (const r of resources) if (r.key && r.checksum) this.checksums.set(r.key, r.checksum);
627
713
  for (const key of this.checksums.keys()) if (this.checksums.has(`${key}.liquid`)) this.checksums.delete(key);
628
714
  }
629
715
  hasChanged(file) {
@@ -633,14 +719,13 @@ var Syncer = class {
633
719
  return [...this.checksums.keys()];
634
720
  }
635
721
  async uploadFile(file) {
636
- const path = `/api/application_themes/${this.themeId}/resources`;
637
- if (file.isText) await this.api.put(path, { application_theme_resource: {
722
+ if (file.isText) await updateThemeResource(this.api, this.themeId, { application_theme_resource: {
638
723
  key: file.relativePath,
639
724
  content: file.read()
640
725
  } });
641
- else await this.uploadBinaryFile(file, path);
726
+ else await this.uploadBinaryFile(file);
642
727
  }
643
- async uploadBinaryFile(file, resourcePath) {
728
+ async uploadBinaryFile(file) {
644
729
  const asset = (await this.api.post("/api/dam/assets", { placeholder_asset: {
645
730
  description: `Uploaded via Fluid CLI: ${file.name}`,
646
731
  mime_type: file.mime.name,
@@ -675,7 +760,7 @@ var Syncer = class {
675
760
  if (ikBody.height) backfillPayload["asset"]["height"] = ikBody.height;
676
761
  if (ikBody.width) backfillPayload["asset"]["width"] = ikBody.width;
677
762
  const backfillBody = await this.api.post("/api/dam/assets/backfill_imagekit", backfillPayload);
678
- await this.api.put(resourcePath, { application_theme_resource: {
763
+ await updateThemeResource(this.api, this.themeId, { application_theme_resource: {
679
764
  key: file.relativePath,
680
765
  dam_asset: {
681
766
  dam_asset_code: backfillBody.asset.code,
@@ -702,13 +787,13 @@ var Syncer = class {
702
787
  }[category] ?? "files"}/${assetCode}`;
703
788
  }
704
789
  async deleteRemoteFile(relativePath) {
705
- await this.api.delete(`/api/application_themes/${this.themeId}/resources`, { body: { application_theme_resource: { key: relativePath } } });
790
+ await deleteThemeResource(this.api, this.themeId, { application_theme_resource: { key: relativePath } });
706
791
  this.checksums.delete(relativePath);
707
792
  }
708
793
  async downloadAll() {
709
- const body = await this.api.get(`/api/application_themes/${this.themeId}/resources`);
710
- this.updateChecksums(body.application_theme_resources ?? []);
711
- return body.application_theme_resources ?? [];
794
+ const resources = (await listThemeResources(this.api, this.themeId)).application_theme_resources ?? [];
795
+ this.updateChecksums(resources);
796
+ return resources;
712
797
  }
713
798
  async downloadBinaryAsset(url) {
714
799
  const resp = await fetch(url);
@@ -767,7 +852,10 @@ var Syncer = class {
767
852
  if (resource.resource_type === "FileResource" && resource.url) {
768
853
  const buf = await this.downloadBinaryAsset(resource.url);
769
854
  file.write(buf);
770
- } else if (resource.content !== void 0) file.write(resource.content);
855
+ } else if (resource.content !== void 0 && resource.content !== null) {
856
+ const content = typeof resource.content === "string" ? resource.content : JSON.stringify(resource.content);
857
+ file.write(content);
858
+ }
771
859
  result.downloaded++;
772
860
  } catch (e) {
773
861
  result.errors.push(`Download ${resource.key}: ${e}`);
@@ -856,7 +944,7 @@ async function startDevServer(api, theme, themeRoot, opts, onReady) {
856
944
  //#region src/commands/dev.ts
857
945
  async function ensureDevTheme(api, identifier) {
858
946
  if (identifier) {
859
- const body = await api.get("/api/application_themes");
947
+ const body = await listApplicationThemes(api);
860
948
  const found = (body.application_themes ?? []).find((t) => String(t.id) === identifier) ?? (body.application_themes ?? []).find((t) => t.name.toLowerCase() === identifier.toLowerCase());
861
949
  if (!found) {
862
950
  console.error(`Theme not found: ${identifier}`);
@@ -866,17 +954,16 @@ async function ensureDevTheme(api, identifier) {
866
954
  }
867
955
  const { devThemeId } = getPluginState();
868
956
  if (devThemeId) try {
869
- const body = await api.get(`/api/application_themes/${devThemeId}`);
957
+ const body = await getApplicationTheme(api, devThemeId);
870
958
  if (body.application_theme) {
871
959
  console.log(`Using existing dev theme #${devThemeId}`);
872
960
  return body.application_theme;
873
961
  }
874
962
  } catch {}
875
963
  const { hostname } = await import("node:os");
876
- const name = `Development (${hostname().split(".")[0] ?? "dev"}-${Math.random().toString(36).slice(2, 8)})`.slice(0, 50);
877
- const theme = (await api.post("/api/application_themes", { application_theme: {
878
- name,
879
- role: "development"
964
+ const theme = (await createApplicationTheme(api, { application_theme: {
965
+ name: `Development (${hostname().split(".")[0] ?? "dev"}-${Math.random().toString(36).slice(2, 8)})`.slice(0, 50),
966
+ status: "development"
880
967
  } })).application_theme;
881
968
  setPluginState({
882
969
  devThemeId: theme.id,
@@ -917,7 +1004,7 @@ function createDevCommand() {
917
1004
  id: theme.id,
918
1005
  name: theme.name,
919
1006
  company,
920
- editorUrl: theme.editor_url
1007
+ editorUrl: theme.editor_url ?? void 0
921
1008
  }, themeRoot, {
922
1009
  host: opts.host,
923
1010
  port,
@@ -934,8 +1021,8 @@ function createDevCommand() {
934
1021
  //#endregion
935
1022
  //#region src/commands/push.ts
936
1023
  async function selectTheme(api) {
937
- const themes = (await api.get("/api/application_themes")).application_themes ?? [];
938
- if (!themes.length) {
1024
+ const themeList = (await listApplicationThemes(api)).application_themes ?? [];
1025
+ if (!themeList.length) {
939
1026
  console.error("No themes found.");
940
1027
  process.exit(1);
941
1028
  }
@@ -943,7 +1030,7 @@ async function selectTheme(api) {
943
1030
  type: "select",
944
1031
  name: "id",
945
1032
  message: "Select a theme to push to",
946
- choices: themes.map((t) => ({
1033
+ choices: themeList.map((t) => ({
947
1034
  title: `${t.name} (#${t.id})`,
948
1035
  value: t.id
949
1036
  }))
@@ -952,11 +1039,11 @@ async function selectTheme(api) {
952
1039
  console.error("No theme selected.");
953
1040
  process.exit(1);
954
1041
  }
955
- return themes.find((t) => t.id === id);
1042
+ return themeList.find((t) => t.id === id);
956
1043
  }
957
1044
  async function findTheme(api, identifier) {
958
- const themes = (await api.get("/api/application_themes")).application_themes ?? [];
959
- const found = themes.find((t) => String(t.id) === identifier) ?? themes.find((t) => t.name.toLowerCase() === identifier.toLowerCase());
1045
+ const themeList = (await listApplicationThemes(api)).application_themes ?? [];
1046
+ const found = themeList.find((t) => String(t.id) === identifier) ?? themeList.find((t) => t.name.toLowerCase() === identifier.toLowerCase());
960
1047
  if (!found) {
961
1048
  console.error(`No theme found with identifier: ${identifier}`);
962
1049
  process.exit(1);
@@ -988,7 +1075,7 @@ function createPushCommand() {
988
1075
  if (opts.publish) {
989
1076
  const pubSpinner = ora("Publishing theme…").start();
990
1077
  try {
991
- await api.post(`/api/application_themes/${theme.id}/publish`);
1078
+ await publishApplicationTheme(api, theme.id);
992
1079
  pubSpinner.succeed("Theme published.");
993
1080
  } catch (e) {
994
1081
  pubSpinner.fail(`Publish failed: ${e}`);
@@ -999,13 +1086,13 @@ function createPushCommand() {
999
1086
  //#endregion
1000
1087
  //#region src/commands/pull.ts
1001
1088
  async function selectOrFindTheme(api, identifier) {
1002
- const themes = (await api.get("/api/application_themes")).application_themes ?? [];
1003
- if (!themes.length) {
1089
+ const themeList = (await listApplicationThemes(api)).application_themes ?? [];
1090
+ if (!themeList.length) {
1004
1091
  console.error("No themes found.");
1005
1092
  process.exit(1);
1006
1093
  }
1007
1094
  if (identifier) {
1008
- const found = themes.find((t) => String(t.id) === identifier) ?? themes.find((t) => t.name.toLowerCase() === identifier.toLowerCase());
1095
+ const found = themeList.find((t) => String(t.id) === identifier) ?? themeList.find((t) => t.name.toLowerCase() === identifier.toLowerCase());
1009
1096
  if (!found) {
1010
1097
  console.error(`No theme found with identifier: ${identifier}`);
1011
1098
  process.exit(1);
@@ -1016,7 +1103,7 @@ async function selectOrFindTheme(api, identifier) {
1016
1103
  type: "select",
1017
1104
  name: "id",
1018
1105
  message: "Select a theme to pull",
1019
- choices: themes.map((t) => ({
1106
+ choices: themeList.map((t) => ({
1020
1107
  title: `${t.name} (#${t.id})`,
1021
1108
  value: t.id
1022
1109
  }))
@@ -1025,7 +1112,7 @@ async function selectOrFindTheme(api, identifier) {
1025
1112
  console.error("No theme selected.");
1026
1113
  process.exit(1);
1027
1114
  }
1028
- return themes.find((t) => t.id === id);
1115
+ return themeList.find((t) => t.id === id);
1029
1116
  }
1030
1117
  function createPullCommand() {
1031
1118
  return new Command("pull").description("Pull a remote theme to your local directory").option("-t, --theme <name-or-id>", "Theme name or ID to pull").option("-n, --nodelete", "Do not delete local files missing on remote").option("--root <path>", "Theme root directory", ".").action(async (opts) => {
@@ -1193,7 +1280,7 @@ function createNavigateCommand() {
1193
1280
  let path;
1194
1281
  if (typeof dest === "string") path = dest;
1195
1282
  else {
1196
- const resources = (await createApiClient().get(`/api/application_themes/${themeId}/available_themeables`, {
1283
+ const resources = (await getApplicationThemeAvailableThemeables(createApiClient(), themeId, {
1197
1284
  themeable: dest.resourceType,
1198
1285
  per_page: 50
1199
1286
  })).available_themeables ?? [];
@@ -1206,7 +1293,7 @@ function createNavigateCommand() {
1206
1293
  name: "slug",
1207
1294
  message: `Select a ${dest.label.toLowerCase()}`,
1208
1295
  choices: resources.map((r) => ({
1209
- title: r.title ?? r.slug,
1296
+ title: r.title ?? r.slug ?? "Untitled",
1210
1297
  value: r.slug
1211
1298
  }))
1212
1299
  }, { onCancel });
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../platform/api-client-core/src/fetch-client.ts","../src/api.ts","../src/plugin-state.ts","../src/theme/mime-type.ts","../src/theme/file.ts","../src/theme/fluid-ignore.ts","../src/theme/root.ts","../src/theme/dev-server/sse.ts","../src/theme/dev-server/hot-reload.ts","../src/theme/dev-server/proxy.ts","../src/theme/dev-server/watcher.ts","../src/theme/syncer.ts","../src/theme/dev-server/index.ts","../src/commands/dev.ts","../src/commands/push.ts","../src/commands/pull.ts","../src/commands/init.ts","../src/commands/navigate.ts","../src/commands/theme.ts","../src/index.ts"],"sourcesContent":["/**\n * Minimal, framework-agnostic fetch client for Fluid APIs\n * Compatible with fluid-admin patterns but usable standalone\n */\n\nexport interface FetchClientConfig {\n /**\n * Base URL for all requests (e.g., \"https://api.fluid.app/api\")\n */\n baseUrl: string;\n\n /**\n * Optional function to get auth token\n * Return null/undefined if no token available\n */\n getAuthToken?: () => string | null | Promise<string | null>;\n\n /**\n * Optional callback when 401 auth error occurs\n */\n onAuthError?: () => void;\n\n /**\n * Default headers to include in all requests\n * Example: { \"x-fluid-client\": \"admin\" }\n */\n defaultHeaders?: Record<string, string>;\n}\n\nexport interface RequestOptions {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\";\n headers?: Record<string, string>;\n params?: Record<string, unknown>;\n body?: unknown;\n signal?: AbortSignal;\n}\n\n/**\n * API Error class compatible with fluid-admin's ApiError\n */\nexport class ApiError extends Error {\n public readonly status: number;\n public readonly data: unknown;\n\n constructor(message: string, status: number, data?: unknown) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.data = data;\n\n if (\"captureStackTrace\" in Error) {\n (\n Error as {\n captureStackTrace: (\n target: Error,\n constructor: NewableFunction,\n ) => void;\n }\n ).captureStackTrace(this, ApiError);\n }\n }\n\n toJSON(): { name: string; message: string; status: number; data: unknown } {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n data: this.data,\n };\n }\n}\n\n/**\n * Type guard for ApiError\n */\nexport function isApiError(error: unknown): error is ApiError {\n return error instanceof ApiError;\n}\n\nexport interface FetchClientInstance {\n request: <TResponse = unknown>(\n endpoint: string,\n options?: RequestOptions,\n ) => Promise<TResponse>;\n requestWithFormData: <TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options?: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n },\n ) => Promise<TResponse>;\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ) => Promise<TResponse>;\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ) => Promise<TResponse>;\n}\n\n/**\n * Creates a configured fetch client instance\n */\nexport function createFetchClient(\n config: FetchClientConfig,\n): FetchClientInstance {\n const { baseUrl, getAuthToken, onAuthError, defaultHeaders = {} } = config;\n\n /**\n * Build headers for a request\n */\n async function buildHeaders(\n customHeaders?: Record<string, string>,\n ): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...defaultHeaders,\n ...customHeaders,\n };\n\n // Add auth token if available\n if (getAuthToken) {\n const token = await getAuthToken();\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n }\n\n return headers;\n }\n\n /**\n * Join baseUrl + endpoint via string concatenation (matches fetchApi).\n * Using `new URL(endpoint, baseUrl)` would strip any path prefix from\n * baseUrl (e.g. \"/api\") when the endpoint starts with \"/\".\n */\n function joinUrl(endpoint: string): string {\n return `${baseUrl}${endpoint}`;\n }\n\n /**\n * Build URL with query parameters for GET requests\n * Compatible with fluid-admin's query param handling\n */\n function buildUrl(\n endpoint: string,\n params?: Record<string, unknown>,\n ): string {\n const fullUrl = joinUrl(endpoint);\n\n if (!params || Object.keys(params).length === 0) {\n return fullUrl;\n }\n\n const queryString = new URLSearchParams();\n\n Object.entries(params).forEach(([key, value]) => {\n if (value === undefined || value === null) {\n return; // Skip undefined/null values\n }\n\n if (Array.isArray(value)) {\n // Handle arrays like Rails expects: key[]\n value.forEach((item) => queryString.append(`${key}[]`, String(item)));\n } else if (typeof value === \"object\") {\n // Handle nested objects: key[subkey]\n Object.entries(value).forEach(([subKey, subValue]) => {\n if (subValue === undefined || subValue === null) {\n return;\n }\n\n if (Array.isArray(subValue)) {\n subValue.forEach((item) =>\n queryString.append(`${key}[${subKey}][]`, String(item)),\n );\n } else {\n queryString.append(`${key}[${subKey}]`, String(subValue));\n }\n });\n } else {\n queryString.append(key, String(value));\n }\n });\n\n const qs = queryString.toString();\n return qs ? `${fullUrl}?${qs}` : fullUrl;\n }\n\n /**\n * Shared response handler for both JSON and FormData requests.\n * Handles auth errors, non-OK responses, 204 No Content, and JSON parsing.\n */\n async function handleResponse<TResponse>(\n response: Response,\n method: string,\n _url: string,\n ): Promise<TResponse> {\n if (response.status === 401 && onAuthError) {\n onAuthError();\n }\n\n if (!response.ok) {\n // Read body as text first to avoid SyntaxError from response.json()\n // when server returns non-JSON bodies with application/json content-type.\n const errorText = await response.text().catch(() => \"\");\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(errorText);\n } catch {\n throw new ApiError(\n errorText.slice(0, 200) ||\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n );\n }\n const msg = (data.message || data.error_message) as string | undefined;\n throw new ApiError(\n msg || `${method} request failed`,\n response.status,\n data.errors || data,\n );\n } else {\n throw new ApiError(\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n );\n }\n }\n\n if (\n response.status === 204 ||\n response.headers.get(\"content-length\") === \"0\"\n ) {\n return null as TResponse;\n }\n\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n try {\n const data = await response.json();\n return data as TResponse;\n } catch {\n try {\n // API declared JSON content-type but body isn't valid JSON\n const text = await response.text();\n return text as TResponse;\n } catch {\n return null as TResponse;\n }\n }\n }\n\n // Non-JSON response (text/plain, text/html, etc.)\n return null as TResponse;\n }\n\n /**\n * Main request function\n */\n async function request<TResponse = unknown>(\n endpoint: string,\n options: RequestOptions = {},\n ): Promise<TResponse> {\n const {\n method = \"GET\",\n headers: customHeaders,\n params,\n body,\n signal,\n } = options;\n\n const url = params ? buildUrl(endpoint, params) : joinUrl(endpoint);\n\n const headers = await buildHeaders(customHeaders);\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers };\n const serializedBody =\n body && method !== \"GET\" ? JSON.stringify(body) : null;\n if (serializedBody) fetchOptions.body = serializedBody;\n if (signal) fetchOptions.signal = signal;\n response = await fetch(url, fetchOptions);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n /**\n * Request with FormData (for file uploads)\n */\n async function requestWithFormData<TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n } = {},\n ): Promise<TResponse> {\n const { method = \"POST\", headers: customHeaders, signal } = options;\n\n const url = joinUrl(endpoint);\n const headers = await buildHeaders(customHeaders);\n\n // Remove Content-Type to let browser set it with boundary\n delete headers[\"Content-Type\"];\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers, body: formData };\n if (signal) fetchOptions.signal = signal;\n response = await fetch(url, fetchOptions);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n // Return client with convenience methods\n return {\n request: request,\n requestWithFormData: requestWithFormData,\n\n // Convenience methods for common HTTP verbs\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"GET\" as const,\n ...(params && { params }),\n }),\n\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"POST\",\n body,\n }),\n\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PUT\",\n body,\n }),\n\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PATCH\",\n body,\n }),\n\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"DELETE\",\n }),\n };\n}\n\nexport type FetchClient = FetchClientInstance;\n","import { createFetchClient } from \"@fluid-app/api-client-core\";\nimport type { FetchClientInstance } from \"@fluid-app/api-client-core\";\nimport { getAuthToken } from \"@fluid-app/fluid-cli\";\n\nexport type ApiClient = FetchClientInstance;\n\nfunction getApiBase(): string {\n return process.env[\"FLUID_API_BASE\"] ?? \"https://api.fluid.app\";\n}\n\nexport function createApiClient(tokenOverride?: string): ApiClient {\n return createFetchClient({\n baseUrl: getApiBase(),\n getAuthToken: () => tokenOverride ?? getAuthToken() ?? null,\n });\n}\n\nexport function requireToken(): string {\n const token = getAuthToken();\n if (!token) {\n console.error(\"Not logged in. Run `fluid login` first.\");\n process.exit(1);\n }\n return token;\n}\n","import { readConfig, updateConfig } from \"@fluid-app/fluid-cli\";\n\ninterface ThemeDevState {\n devThemeId?: number;\n devThemeName?: string;\n}\n\nconst PLUGIN_KEY = \"theme-dev\";\n\nexport function getPluginState(): ThemeDevState {\n const config = readConfig();\n return (config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {};\n}\n\nexport function setPluginState(updates: Partial<ThemeDevState>): void {\n updateConfig((config) => ({\n ...config,\n plugins: {\n ...config.plugins,\n [PLUGIN_KEY]: {\n ...((config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {}),\n ...updates,\n },\n },\n }));\n}\n","const TEXT_TYPES: Record<string, string> = {\n \".liquid\": \"text/x-liquid\",\n \".json\": \"application/json\",\n \".css\": \"text/css\",\n \".js\": \"application/javascript\",\n \".html\": \"text/html\",\n \".txt\": \"text/plain\",\n \".md\": \"text/markdown\",\n \".svg\": \"image/svg+xml\",\n};\n\nconst BINARY_TYPES: Record<string, string> = {\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".eot\": \"application/vnd.ms-fontobject\",\n \".otf\": \"font/otf\",\n \".pdf\": \"application/pdf\",\n \".zip\": \"application/zip\",\n \".mp4\": \"video/mp4\",\n \".webm\": \"video/webm\",\n \".mp3\": \"audio/mpeg\",\n \".wav\": \"audio/wav\",\n};\n\nexport interface MimeType {\n name: string;\n isText: boolean;\n}\n\nexport function mimeTypeFor(ext: string): MimeType {\n const text = TEXT_TYPES[ext];\n if (text) return { name: text, isText: true };\n\n const binary = BINARY_TYPES[ext];\n if (binary) return { name: binary, isText: false };\n\n return { name: \"application/octet-stream\", isText: false };\n}\n","import {\n readFileSync,\n writeFileSync,\n mkdirSync,\n existsSync,\n statSync,\n} from \"node:fs\";\nimport { extname, basename, relative, dirname } from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { mimeTypeFor, type MimeType } from \"./mime-type.js\";\n\nexport class ThemeFile {\n readonly absolutePath: string;\n readonly relativePath: string;\n readonly mime: MimeType;\n\n constructor(absolutePath: string, root: string) {\n this.absolutePath = absolutePath;\n this.relativePath = relative(root, absolutePath);\n this.mime = mimeTypeFor(extname(absolutePath).toLowerCase());\n }\n\n get name(): string {\n return basename(this.absolutePath);\n }\n\n get isText(): boolean {\n return this.mime.isText;\n }\n\n get isLiquid(): boolean {\n return this.absolutePath.endsWith(\".liquid\");\n }\n\n get isJson(): boolean {\n return this.absolutePath.endsWith(\".json\");\n }\n\n get exists(): boolean {\n return existsSync(this.absolutePath);\n }\n\n read(): string {\n return readFileSync(this.absolutePath, \"utf-8\");\n }\n\n readBinary(): Buffer {\n return readFileSync(this.absolutePath);\n }\n\n write(content: string | Buffer): void {\n mkdirSync(dirname(this.absolutePath), { recursive: true });\n if (typeof content === \"string\") {\n writeFileSync(this.absolutePath, content, \"utf-8\");\n } else {\n writeFileSync(this.absolutePath, content);\n }\n }\n\n checksum(): string {\n const content = this.isText ? this.read() : this.readBinary();\n return createHash(\"sha256\").update(content).digest(\"hex\");\n }\n\n size(): number {\n return statSync(this.absolutePath).size;\n }\n}\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\n\nconst IGNORE_FILE = \".fluidignore\";\n\ninterface Pattern {\n negated: boolean;\n pattern: string;\n}\n\nexport class FluidIgnore {\n private patterns: Pattern[];\n\n constructor(root: string) {\n this.patterns = this.parse(join(root, IGNORE_FILE));\n }\n\n ignore(relativePath: string): boolean {\n let result = false;\n for (const { negated, pattern } of this.patterns) {\n if (this.match(pattern, relativePath)) {\n result = !negated;\n }\n }\n return result;\n }\n\n private parse(filePath: string): Pattern[] {\n if (!existsSync(filePath)) return [];\n return readFileSync(filePath, \"utf-8\")\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l && !l.startsWith(\"#\"))\n .map((l) => {\n const negated = l.startsWith(\"!\");\n let pattern = negated ? l.slice(1) : l;\n if (pattern.startsWith(\"/\")) pattern = pattern.slice(1);\n return { negated, pattern };\n });\n }\n\n private match(pattern: string, path: string): boolean {\n if (pattern.endsWith(\"/\")) {\n return path.startsWith(pattern) || path === pattern.slice(0, -1);\n }\n if (pattern.includes(\"/\")) {\n return this.fnmatch(pattern, path);\n }\n return this.fnmatch(pattern, path) || this.fnmatch(pattern, basename(path));\n }\n\n private fnmatch(pattern: string, str: string): boolean {\n const re = pattern\n .split(\"**\")\n .map((p) =>\n p\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/\\?/g, \"[^/]\"),\n )\n .join(\".*\");\n return new RegExp(`^${re}$`).test(str);\n }\n}\n","import { readdirSync, statSync } from \"node:fs\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport { ThemeFile } from \"./file.js\";\nimport { FluidIgnore } from \"./fluid-ignore.js\";\n\nconst THEME_MARKERS = [\"templates\", \"assets\", \"config\"];\n\nexport class ThemeRoot {\n readonly root: string;\n readonly ignore: FluidIgnore;\n\n constructor(root: string) {\n this.root = resolve(root);\n this.ignore = new FluidIgnore(this.root);\n }\n\n isValid(): boolean {\n return THEME_MARKERS.some((m) => {\n try {\n return statSync(join(this.root, m)).isDirectory();\n } catch {\n return false;\n }\n });\n }\n\n files(): ThemeFile[] {\n return this.glob(this.root).filter(\n (f) => !this.ignore.ignore(f.relativePath),\n );\n }\n\n file(pathOrFile: string | ThemeFile): ThemeFile {\n if (pathOrFile instanceof ThemeFile) return pathOrFile;\n const abs = isAbsolute(pathOrFile)\n ? pathOrFile\n : join(this.root, pathOrFile);\n return new ThemeFile(abs, this.root);\n }\n\n private glob(dir: string): ThemeFile[] {\n const results: ThemeFile[] = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name.startsWith(\".\")) continue;\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...this.glob(full));\n } else if (entry.isFile()) {\n results.push(new ThemeFile(full, this.root));\n }\n }\n return results;\n }\n}\n","import type { ServerResponse } from \"node:http\";\n\nexport class SSEStream {\n private responses = new Set<ServerResponse>();\n\n add(res: ServerResponse): void {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n \"Access-Control-Allow-Origin\": \"*\",\n });\n res.write(\":\\n\\n\");\n this.responses.add(res);\n res.on(\"close\", () => this.responses.delete(res));\n }\n\n broadcast(data: string): void {\n const payload = `data: ${data}\\n\\n`;\n for (const res of this.responses) {\n try {\n res.write(payload);\n } catch {\n this.responses.delete(res);\n }\n }\n }\n\n close(): void {\n for (const res of this.responses) {\n try {\n res.end();\n } catch {\n // ignore\n }\n }\n this.responses.clear();\n }\n\n get size(): number {\n return this.responses.size;\n }\n}\n","export function buildHotReloadScript(mode: \"full-page\" | \"off\"): string {\n return `\n<script>\n(() => {\n window.__FLUID_CLI_ENV__ = ${JSON.stringify({ mode })};\n\n class HotReload {\n static reloadMode() { return window.__FLUID_CLI_ENV__.mode; }\n static isActive() { return HotReload.reloadMode() !== \"off\"; }\n static setHotReloadCookie(files) {\n const expires = new Date(Date.now() + 3000).toUTCString();\n document.cookie = \\`hot_reload_files=\\${files.join(\",\")};expires=\\${expires};path=/\\`;\n }\n static refresh(files) {\n HotReload.setHotReloadCookie(files);\n console.log(\"[HotReload] Refreshing page\");\n window.location.reload();\n }\n }\n\n class SSEClient {\n constructor(url, handler) {\n if (typeof EventSource === \"undefined\") {\n console.error(\"[HotReload] EventSource not supported in this browser.\");\n return;\n }\n console.log(\"[HotReload] Initializing…\");\n this.url = url;\n this.handler = handler;\n }\n connect() {\n const es = new EventSource(this.url);\n es.onopen = () => console.log(\"[HotReload] SSE connected.\");\n es.onerror = () => {\n console.log(\"[HotReload] SSE closed. Reconnecting in 5s…\");\n es.close();\n setTimeout(() => this.connect(), 5000);\n };\n es.onmessage = (msg) => {\n const data = JSON.parse(msg.data);\n if (data.reload_page) { HotReload.refresh([]); return; }\n this.handler(data);\n };\n }\n }\n\n if (HotReload.isActive()) {\n new SSEClient(\"/hot-reload\", (data) => {\n if (data.modified) HotReload.refresh(data.modified);\n }).connect();\n }\n})();\n</script>`;\n}\n\nexport function injectHotReload(\n html: string,\n mode: \"full-page\" | \"off\",\n): string {\n const script = buildHotReloadScript(mode);\n if (html.includes(\"</body>\")) {\n return html.replace(\"</body>\", `${script}\\n</body>`);\n }\n return html + script;\n}\n","import https from \"node:https\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { injectHotReload } from \"./hot-reload.js\";\nimport { getAuthToken } from \"@fluid-app/fluid-cli\";\n\nconst HOP_BY_HOP = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n \"content-security-policy\",\n]);\n\nexport interface ProxyOptions {\n company: string;\n themeId: number;\n reloadMode: \"full-page\" | \"off\";\n pendingFiles?: () => Array<{ relativePath: string; read: () => string }>;\n}\n\nexport async function proxyRequest(\n req: IncomingMessage,\n res: ServerResponse,\n opts: ProxyOptions,\n): Promise<void> {\n const companyHost = `${opts.company}.fluid.app`;\n\n const headers: Record<string, string> = {};\n for (const [k, v] of Object.entries(req.headers)) {\n if (!HOP_BY_HOP.has(k.toLowerCase()) && typeof v === \"string\") {\n headers[k] = v;\n }\n }\n headers[\"host\"] = companyHost;\n headers[\"x-fluid-theme\"] = String(opts.themeId);\n headers[\"user-agent\"] = \"Fluid CLI\";\n headers[\"accept-encoding\"] = \"identity\";\n\n const url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n url.searchParams.set(\"_fd\", \"0\");\n url.searchParams.set(\"pb\", \"0\");\n\n const pending = opts.pendingFiles?.() ?? [];\n const isGet = req.method === \"GET\" || req.method === \"HEAD\";\n let method = req.method ?? \"GET\";\n let body: string | Buffer | undefined;\n\n if (pending.length > 0 && isGet) {\n method = \"POST\";\n const params = new URLSearchParams();\n params.set(\"_method\", req.method ?? \"GET\");\n for (const f of pending) {\n params.set(`replace_templates[${f.relativePath}]`, f.read());\n }\n const token = getAuthToken();\n if (token) headers[\"authorization\"] = `Bearer ${token}`;\n headers[\"content-type\"] = \"application/x-www-form-urlencoded\";\n body = params.toString();\n headers[\"content-length\"] = String(Buffer.byteLength(body));\n } else if (!isGet) {\n body = await readBody(req);\n if (body.length > 0) {\n headers[\"content-length\"] = String(body.length);\n }\n }\n\n return new Promise((resolve, reject) => {\n const options: https.RequestOptions = {\n hostname: companyHost,\n port: 443,\n path: url.pathname + (url.search || \"\"),\n method,\n headers,\n };\n\n const proxyReq = https.request(options, (proxyRes) => {\n const contentType = proxyRes.headers[\"content-type\"] ?? \"\";\n const isHtml = contentType.includes(\"text/html\");\n\n const responseHeaders: Record<string, string | string[]> = {};\n for (const [k, v] of Object.entries(proxyRes.headers)) {\n if (!HOP_BY_HOP.has(k.toLowerCase()) && v !== undefined) {\n responseHeaders[k] = v as string | string[];\n }\n }\n\n if (isHtml) {\n const chunks: Buffer[] = [];\n proxyRes.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n proxyRes.on(\"end\", () => {\n let html = Buffer.concat(chunks).toString(\"utf-8\");\n html = injectHotReload(html, opts.reloadMode);\n responseHeaders[\"content-length\"] = String(Buffer.byteLength(html));\n res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);\n res.end(html);\n resolve();\n });\n } else {\n res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);\n proxyRes.pipe(res);\n proxyRes.on(\"end\", resolve);\n }\n });\n\n proxyReq.on(\"error\", (err) => {\n reject(err);\n });\n\n if (body) proxyReq.write(body);\n proxyReq.end();\n });\n}\n\nfunction readBody(req: IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n","import { relative } from \"node:path\";\nimport chokidar from \"chokidar\";\nimport type { ThemeRoot } from \"../root.js\";\nimport type { ThemeFile } from \"../file.js\";\n\nexport type FileChangeHandler = (\n modified: ThemeFile[],\n added: ThemeFile[],\n removed: ThemeFile[],\n) => Promise<void>;\n\nexport function watchTheme(\n root: ThemeRoot,\n handler: FileChangeHandler,\n): () => Promise<void> {\n const watcher = chokidar.watch(root.root, {\n ignoreInitial: true,\n ignored: (filePath: string) => {\n if (filePath.includes(\"node_modules\")) return true;\n try {\n const rel = relative(root.root, filePath);\n const basename = rel.split(/[\\\\/]/).pop() ?? \"\";\n return basename.startsWith(\".\") || root.ignore.ignore(rel);\n } catch {\n return false;\n }\n },\n persistent: true,\n awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 },\n });\n\n let pending = Promise.resolve();\n const enqueue = (fn: () => Promise<void>) => {\n pending = pending.then(fn).catch(() => {});\n };\n\n watcher.on(\"change\", (filePath) => {\n const rel = relative(root.root, filePath);\n if (root.ignore.ignore(rel)) return;\n enqueue(() => handler([root.file(filePath)], [], []));\n });\n\n watcher.on(\"add\", (filePath) => {\n const rel = relative(root.root, filePath);\n if (root.ignore.ignore(rel)) return;\n enqueue(() => handler([], [root.file(filePath)], []));\n });\n\n watcher.on(\"unlink\", (filePath) => {\n enqueue(() => handler([], [], [root.file(filePath)]));\n });\n\n return () => watcher.close();\n}\n","import { sep } from \"node:path\";\nimport type { ApiClient } from \"../api.js\";\nimport type { ThemeFile } from \"./file.js\";\nimport type { ThemeRoot } from \"./root.js\";\n\nexport interface RemoteResource {\n key: string;\n checksum: string;\n content?: string;\n url?: string;\n resource_type: string;\n}\n\nexport interface SyncResult {\n uploaded: number;\n downloaded: number;\n deleted: number;\n errors: string[];\n}\n\nexport class Syncer {\n private checksums = new Map<string, string>();\n\n constructor(\n private api: ApiClient,\n private themeId: number,\n private themeRoot: ThemeRoot,\n ) {}\n\n // ─── Checksum Management ──────────────────────────────────────────────────\n\n async fetchChecksums(): Promise<void> {\n const body = await this.api.get<{\n application_theme_resources: RemoteResource[];\n }>(`/api/application_themes/${this.themeId}/resources`);\n this.updateChecksums(body.application_theme_resources ?? []);\n }\n\n private updateChecksums(resources: RemoteResource[]): void {\n for (const r of resources) {\n if (r.key) this.checksums.set(r.key, r.checksum);\n }\n for (const key of this.checksums.keys()) {\n if (this.checksums.has(`${key}.liquid`)) this.checksums.delete(key);\n }\n }\n\n hasChanged(file: ThemeFile): boolean {\n return file.checksum() !== this.checksums.get(file.relativePath);\n }\n\n remoteKeys(): string[] {\n return [...this.checksums.keys()];\n }\n\n // ─── Upload ───────────────────────────────────────────────────────────────\n\n async uploadFile(file: ThemeFile): Promise<void> {\n const path = `/api/application_themes/${this.themeId}/resources`;\n if (file.isText) {\n await this.api.put(path, {\n application_theme_resource: {\n key: file.relativePath,\n content: file.read(),\n },\n });\n } else {\n await this.uploadBinaryFile(file, path);\n }\n }\n\n private async uploadBinaryFile(\n file: ThemeFile,\n resourcePath: string,\n ): Promise<void> {\n // Step 1: Create DAM placeholder\n const placeholderBody = await this.api.post<{\n asset: { id: number; canonical_path: string };\n }>(\"/api/dam/assets\", {\n placeholder_asset: {\n description: `Uploaded via Fluid CLI: ${file.name}`,\n mime_type: file.mime.name,\n name: file.name,\n },\n });\n const asset = placeholderBody.asset;\n\n // Step 2: Get ImageKit auth token\n const authBody = await this.api.post<{\n token: string;\n signature: string;\n expire: number;\n }>(\"/api/dam/assets/imagekit_auth\", {});\n\n // Step 3: Upload to ImageKit via multipart\n const folder = this.canonicalPathToImageKitFolder(asset.canonical_path);\n const formData = new FormData();\n const blob = new Blob([file.readBinary() as unknown as ArrayBuffer], {\n type: file.mime.name,\n });\n formData.append(\"file\", blob, file.name);\n formData.append(\"token\", authBody.token);\n formData.append(\"signature\", authBody.signature);\n formData.append(\"expire\", String(authBody.expire));\n formData.append(\"folder\", folder);\n formData.append(\"fileName\", file.name);\n formData.append(\"publicKey\", \"public_j7s4Ih9ETh/OCp41mVQH7tlXBdU=\");\n\n const ikResp = await fetch(\n \"https://upload.imagekit.io/api/v1/files/upload\",\n {\n method: \"POST\",\n body: formData,\n },\n );\n if (!ikResp.ok) throw new Error(`ImageKit upload failed: ${ikResp.status}`);\n const ikBody = (await ikResp.json()) as {\n fileId: string;\n url: string;\n thumbnailUrl: string;\n size: number;\n height?: number;\n width?: number;\n };\n\n // Step 4: Backfill DAM asset\n const backfillPayload: Record<string, unknown> = {\n asset: {\n id: asset.id,\n imagekit_file_id: ikBody.fileId,\n imagekit_url: ikBody.url,\n mime_type: file.mime.name,\n name: file.name,\n file_size: ikBody.size,\n expected_path: asset.canonical_path,\n },\n };\n if (ikBody.height)\n (backfillPayload[\"asset\"] as Record<string, unknown>)[\"height\"] =\n ikBody.height;\n if (ikBody.width)\n (backfillPayload[\"asset\"] as Record<string, unknown>)[\"width\"] =\n ikBody.width;\n\n const backfillBody = await this.api.post<{\n asset: { code: string; default_variant_url: string };\n }>(\"/api/dam/assets/backfill_imagekit\", backfillPayload);\n\n // Step 5: Associate with theme resource\n await this.api.put(resourcePath, {\n application_theme_resource: {\n key: file.relativePath,\n dam_asset: {\n dam_asset_code: backfillBody.asset.code,\n content_type: file.mime.name,\n content_size: ikBody.size,\n filename: file.name,\n handle: backfillBody.asset.code,\n url: backfillBody.asset.default_variant_url,\n preview_image_url: ikBody.thumbnailUrl,\n },\n },\n });\n }\n\n private canonicalPathToImageKitFolder(canonicalPath: string): string {\n const parts = canonicalPath.split(\".\");\n const companyId = parts[0] ?? \"unknown\";\n const category = parts[1] ?? \"files\";\n const assetCode = parts[2] ?? \"unknown\";\n const folderMap: Record<string, string> = {\n images: \"images\",\n videos: \"videos\",\n audio: \"audio\",\n documents: \"documents\",\n files: \"files\",\n };\n return `${companyId}/${folderMap[category] ?? \"files\"}/${assetCode}`;\n }\n\n // ─── Delete ───────────────────────────────────────────────────────────────\n\n async deleteRemoteFile(relativePath: string): Promise<void> {\n await this.api.delete(`/api/application_themes/${this.themeId}/resources`, {\n body: { application_theme_resource: { key: relativePath } },\n });\n this.checksums.delete(relativePath);\n }\n\n // ─── Download ─────────────────────────────────────────────────────────────\n\n async downloadAll(): Promise<RemoteResource[]> {\n const body = await this.api.get<{\n application_theme_resources: RemoteResource[];\n }>(`/api/application_themes/${this.themeId}/resources`);\n this.updateChecksums(body.application_theme_resources ?? []);\n return body.application_theme_resources ?? [];\n }\n\n async downloadBinaryAsset(url: string): Promise<Buffer> {\n const resp = await fetch(url);\n if (!resp.ok) throw new Error(`Failed to download asset: ${resp.status}`);\n return Buffer.from(await resp.arrayBuffer());\n }\n\n // ─── Full Upload ──────────────────────────────────────────────────────────\n\n async uploadTheme(\n opts: {\n delete?: boolean;\n onProgress?: (done: number, total: number) => void;\n } = {},\n ): Promise<SyncResult> {\n await this.fetchChecksums();\n\n const localFiles = this.themeRoot.files();\n const result: SyncResult = {\n uploaded: 0,\n deleted: 0,\n downloaded: 0,\n errors: [],\n };\n\n const toUpload = localFiles.filter((f) => f.exists && this.hasChanged(f));\n let done = 0;\n for (const file of toUpload) {\n try {\n await this.uploadFile(file);\n result.uploaded++;\n } catch (e) {\n result.errors.push(`Upload ${file.relativePath}: ${e}`);\n }\n opts.onProgress?.(++done, toUpload.length);\n }\n\n if (opts.delete) {\n const localPaths = new Set(localFiles.map((f) => f.relativePath));\n const toDelete = this.remoteKeys().filter((k) => !localPaths.has(k));\n for (const key of toDelete) {\n try {\n await this.deleteRemoteFile(key);\n result.deleted++;\n } catch (e) {\n result.errors.push(`Delete ${key}: ${e}`);\n }\n }\n }\n\n return result;\n }\n\n // ─── Full Download ────────────────────────────────────────────────────────\n\n async downloadTheme(\n opts: {\n delete?: boolean;\n onProgress?: (done: number, total: number) => void;\n } = {},\n ): Promise<SyncResult> {\n const resources = await this.downloadAll();\n const result: SyncResult = {\n uploaded: 0,\n deleted: 0,\n downloaded: 0,\n errors: [],\n };\n\n let done = 0;\n for (const resource of resources) {\n const file = this.themeRoot.file(resource.key);\n\n // Guard against path traversal from malicious API responses\n if (!file.absolutePath.startsWith(this.themeRoot.root + sep)) {\n result.errors.push(`Download ${resource.key}: path traversal detected`);\n opts.onProgress?.(++done, resources.length);\n continue;\n }\n\n try {\n if (resource.resource_type === \"FileResource\" && resource.url) {\n const buf = await this.downloadBinaryAsset(resource.url);\n file.write(buf);\n } else if (resource.content !== undefined) {\n file.write(resource.content);\n }\n result.downloaded++;\n } catch (e) {\n result.errors.push(`Download ${resource.key}: ${e}`);\n }\n opts.onProgress?.(++done, resources.length);\n }\n\n if (opts.delete) {\n const remoteKeys = new Set(resources.map((r) => r.key));\n for (const file of this.themeRoot.files()) {\n if (!remoteKeys.has(file.relativePath)) {\n try {\n const { unlinkSync } = await import(\"node:fs\");\n unlinkSync(file.absolutePath);\n result.deleted++;\n } catch {\n // ignore\n }\n }\n }\n }\n\n return result;\n }\n}\n","import http from \"node:http\";\nimport { SSEStream } from \"./sse.js\";\nimport { proxyRequest } from \"./proxy.js\";\nimport { watchTheme } from \"./watcher.js\";\nimport { Syncer } from \"../syncer.js\";\nimport type { ThemeRoot } from \"../root.js\";\nimport type { ApiClient } from \"../../api.js\";\n\nexport interface DevServerOptions {\n host: string;\n port: number;\n reloadMode: \"full-page\" | \"off\";\n}\n\nexport interface DevServerTheme {\n id: number;\n name: string;\n company: string;\n editorUrl?: string;\n}\n\nexport async function startDevServer(\n api: ApiClient,\n theme: DevServerTheme,\n themeRoot: ThemeRoot,\n opts: DevServerOptions,\n onReady?: (address: string) => void,\n): Promise<() => void> {\n const sse = new SSEStream();\n const syncer = new Syncer(api, theme.id, themeRoot);\n\n const pendingUpdates = new Set<string>();\n\n // ── Initial sync ─────────────────────────────────────────────────────────\n console.log(`\\nSyncing theme ${theme.name} (#${theme.id})…`);\n await syncer.uploadTheme({\n delete: true,\n onProgress: (done, total) => {\n process.stdout.write(`\\r Uploading ${done}/${total} files…`);\n },\n });\n process.stdout.write(\"\\n\");\n\n // ── File watcher ─────────────────────────────────────────────────────────\n const stopWatcher = watchTheme(\n themeRoot,\n async (modified, added, removed) => {\n const changed = [...modified, ...added];\n\n for (const file of changed) {\n pendingUpdates.add(file.relativePath);\n try {\n await syncer.uploadFile(file);\n } catch (e) {\n console.error(\n `\\n[Watcher] Upload failed: ${file.relativePath}: ${e}`,\n );\n } finally {\n pendingUpdates.delete(file.relativePath);\n }\n }\n\n for (const file of removed) {\n try {\n await syncer.deleteRemoteFile(file.relativePath);\n } catch {\n // ignore\n }\n }\n\n if (removed.length > 0) {\n sse.broadcast(JSON.stringify({ reload_page: true }));\n } else if (changed.length > 0) {\n sse.broadcast(\n JSON.stringify({ modified: changed.map((f) => f.relativePath) }),\n );\n }\n },\n );\n\n // ── HTTP server ───────────────────────────────────────────────────────────\n const server = http.createServer(async (req, res) => {\n if (req.url === \"/hot-reload\") {\n sse.add(res);\n return;\n }\n\n try {\n await proxyRequest(req, res, {\n company: theme.company,\n themeId: theme.id,\n reloadMode: opts.reloadMode,\n pendingFiles: () =>\n [...pendingUpdates]\n .map((p) => themeRoot.file(p))\n .filter((f) => f.isText)\n .map((f) => ({\n relativePath: f.relativePath,\n read: () => f.read(),\n })),\n });\n } catch (e) {\n console.error(`[Proxy] ${req.method} ${req.url} β†’ ${e}`);\n if (!res.headersSent) {\n res.writeHead(502);\n res.end(\"Bad Gateway\");\n }\n }\n });\n\n await new Promise<void>((resolve, reject) => {\n server.listen(opts.port, opts.host, () => resolve());\n server.on(\"error\", reject);\n });\n\n const address = `http://${opts.host}:${opts.port}`;\n onReady?.(address);\n\n // ── Teardown ──────────────────────────────────────────────────────────────\n return function stop() {\n sse.close();\n stopWatcher();\n server.close();\n };\n}\n","import { Command } from \"commander\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { getPluginState, setPluginState } from \"../plugin-state.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { startDevServer } from \"../theme/dev-server/index.js\";\n\ninterface ApplicationTheme {\n id: number;\n name: string;\n editor_url?: string;\n}\n\ninterface CompanyMe {\n data: { company: { subdomain?: string; name?: string } };\n}\n\nasync function ensureDevTheme(\n api: ReturnType<typeof createApiClient>,\n identifier?: string,\n): Promise<ApplicationTheme> {\n if (identifier) {\n const body = await api.get<{ application_themes: ApplicationTheme[] }>(\n \"/api/application_themes\",\n );\n const found =\n (body.application_themes ?? []).find(\n (t) => String(t.id) === identifier,\n ) ??\n (body.application_themes ?? []).find(\n (t) => t.name.toLowerCase() === identifier.toLowerCase(),\n );\n if (!found) {\n console.error(`Theme not found: ${identifier}`);\n process.exit(1);\n }\n return found;\n }\n\n // Reuse stored dev theme if it still exists\n const { devThemeId } = getPluginState();\n if (devThemeId) {\n try {\n const body = await api.get<{ application_theme: ApplicationTheme }>(\n `/api/application_themes/${devThemeId}`,\n );\n if (body.application_theme) {\n console.log(`Using existing dev theme #${devThemeId}`);\n return body.application_theme;\n }\n } catch {\n // Theme no longer exists β€” create a new one\n }\n }\n\n // Create a new development theme\n const { hostname } = await import(\"node:os\");\n const host = hostname().split(\".\")[0] ?? \"dev\";\n const name =\n `Development (${host}-${Math.random().toString(36).slice(2, 8)})`.slice(\n 0,\n 50,\n );\n\n const body = await api.post<{ application_theme: ApplicationTheme }>(\n \"/api/application_themes\",\n { application_theme: { name, role: \"development\" } },\n );\n const theme = body.application_theme;\n setPluginState({ devThemeId: theme.id, devThemeName: theme.name });\n console.log(`Created dev theme: ${theme.name} (#${theme.id})`);\n return theme;\n}\n\nexport function createDevCommand(): Command {\n return new Command(\"dev\")\n .description(\"Start the theme dev server with hot reload\")\n .option(\"--host <host>\", \"Local server host\", \"127.0.0.1\")\n .option(\"--port <port>\", \"Local server port\", \"9292\")\n .option(\n \"-t, --theme <name-or-id>\",\n \"Use an existing theme instead of dev theme\",\n )\n .option(\"-f, --force\", \"Skip schema validation on upload\")\n .option(\"--live-reload <mode>\", \"Reload mode: full-page | off\", \"full-page\")\n .option(\"--navigate\", \"Open browser navigator after server starts\")\n .option(\"--root <path>\", \"Theme root directory\", \".\")\n .action(\n async (opts: {\n host: string;\n port: string;\n theme?: string;\n force?: boolean;\n liveReload: string;\n navigate?: boolean;\n root: string;\n }) => {\n requireToken();\n\n const themeRoot = new ThemeRoot(opts.root);\n if (!themeRoot.isValid()) {\n console.error(`'${opts.root}' does not look like a theme directory.`);\n process.exit(1);\n }\n\n const port = Number(opts.port);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n console.error(\n `Invalid port: '${opts.port}'. Must be an integer between 1 and 65535.`,\n );\n process.exit(1);\n }\n\n const reloadMode = opts.liveReload === \"off\" ? \"off\" : \"full-page\";\n const api = createApiClient();\n\n const companyRes = await api.get<CompanyMe>(\n \"/api/company/v1/companies/me\",\n );\n const company = companyRes.data?.company?.subdomain;\n if (!company) {\n console.error(\n \"Could not determine company subdomain. Make sure your token is valid.\",\n );\n process.exit(1);\n }\n\n const theme = await ensureDevTheme(api, opts.theme);\n\n let stop: (() => void) | undefined;\n\n const cleanup = () => {\n stop?.();\n process.exit(0);\n };\n process.on(\"SIGINT\", cleanup);\n process.on(\"SIGTERM\", cleanup);\n\n stop = await startDevServer(\n api,\n {\n id: theme.id,\n name: theme.name,\n company,\n editorUrl: theme.editor_url,\n },\n themeRoot,\n { host: opts.host, port, reloadMode },\n (address) => {\n console.log(`\\n Dev server: ${address}`);\n if (theme.editor_url)\n console.log(` Web editor: ${theme.editor_url}`);\n console.log(\"\\n Watching for file changes…\\n\");\n\n if (opts.navigate) {\n import(\"open\").then((m) => m.default(`${address}/home`));\n }\n },\n );\n\n // Keep process alive\n await new Promise(() => {});\n },\n );\n}\n","import { Command } from \"commander\";\nimport ora from \"ora\";\nimport prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { Syncer } from \"../theme/syncer.js\";\n\ninterface ApplicationTheme {\n id: number;\n name: string;\n}\n\nasync function selectTheme(\n api: ReturnType<typeof createApiClient>,\n): Promise<ApplicationTheme> {\n const body = await api.get<{ application_themes: ApplicationTheme[] }>(\n \"/api/application_themes\",\n );\n const themes = body.application_themes ?? [];\n if (!themes.length) {\n console.error(\"No themes found.\");\n process.exit(1);\n }\n const { id } = await prompts(\n {\n type: \"select\",\n name: \"id\",\n message: \"Select a theme to push to\",\n choices: themes.map((t) => ({\n title: `${t.name} (#${t.id})`,\n value: t.id,\n })),\n },\n { onCancel: () => process.exit(130) },\n );\n if (!id) {\n console.error(\"No theme selected.\");\n process.exit(1);\n }\n return themes.find((t) => t.id === id)!;\n}\n\nasync function findTheme(\n api: ReturnType<typeof createApiClient>,\n identifier: string,\n): Promise<ApplicationTheme> {\n const body = await api.get<{ application_themes: ApplicationTheme[] }>(\n \"/api/application_themes\",\n );\n const themes = body.application_themes ?? [];\n const found =\n themes.find((t) => String(t.id) === identifier) ??\n themes.find((t) => t.name.toLowerCase() === identifier.toLowerCase());\n if (!found) {\n console.error(`No theme found with identifier: ${identifier}`);\n process.exit(1);\n }\n return found;\n}\n\nexport function createPushCommand(): Command {\n return new Command(\"push\")\n .description(\"Push local theme files to a remote theme\")\n .option(\"-t, --theme <name-or-id>\", \"Theme name or ID to push to\")\n .option(\"-n, --nodelete\", \"Do not delete remote files missing locally\")\n .option(\"-f, --force\", \"Skip schema validation\")\n .option(\"-p, --publish\", \"Publish the theme after pushing\")\n .option(\"--root <path>\", \"Theme root directory\", \".\")\n .action(\n async (opts: {\n theme?: string;\n nodelete?: boolean;\n force?: boolean;\n publish?: boolean;\n root: string;\n }) => {\n requireToken();\n\n const themeRoot = new ThemeRoot(opts.root);\n if (!themeRoot.isValid()) {\n console.error(`'${opts.root}' does not look like a theme directory.`);\n process.exit(1);\n }\n\n const api = createApiClient();\n const theme = opts.theme\n ? await findTheme(api, opts.theme)\n : await selectTheme(api);\n\n const syncer = new Syncer(api, theme.id, themeRoot);\n const spinner = ora(`Pushing to ${theme.name} (#${theme.id})…`).start();\n\n const result = await syncer.uploadTheme({\n delete: !opts.nodelete,\n onProgress: (d, total) => {\n spinner.text = `Pushing ${d}/${total} files…`;\n },\n });\n\n if (result.errors.length) {\n spinner.warn(`Pushed with ${result.errors.length} error(s).`);\n for (const e of result.errors) console.error(` ${e}`);\n } else {\n spinner.succeed(\n `Pushed ${result.uploaded} file(s), deleted ${result.deleted} remote file(s).`,\n );\n }\n\n if (opts.publish) {\n const pubSpinner = ora(\"Publishing theme…\").start();\n try {\n await api.post(`/api/application_themes/${theme.id}/publish`);\n pubSpinner.succeed(\"Theme published.\");\n } catch (e) {\n pubSpinner.fail(`Publish failed: ${e}`);\n }\n }\n },\n );\n}\n","import { Command } from \"commander\";\nimport ora from \"ora\";\nimport prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { Syncer } from \"../theme/syncer.js\";\n\ninterface ApplicationTheme {\n id: number;\n name: string;\n}\n\nasync function selectOrFindTheme(\n api: ReturnType<typeof createApiClient>,\n identifier?: string,\n): Promise<ApplicationTheme> {\n const body = await api.get<{ application_themes: ApplicationTheme[] }>(\n \"/api/application_themes\",\n );\n const themes = body.application_themes ?? [];\n if (!themes.length) {\n console.error(\"No themes found.\");\n process.exit(1);\n }\n\n if (identifier) {\n const found =\n themes.find((t) => String(t.id) === identifier) ??\n themes.find((t) => t.name.toLowerCase() === identifier.toLowerCase());\n if (!found) {\n console.error(`No theme found with identifier: ${identifier}`);\n process.exit(1);\n }\n return found;\n }\n\n const { id } = await prompts(\n {\n type: \"select\",\n name: \"id\",\n message: \"Select a theme to pull\",\n choices: themes.map((t) => ({\n title: `${t.name} (#${t.id})`,\n value: t.id,\n })),\n },\n { onCancel: () => process.exit(130) },\n );\n if (!id) {\n console.error(\"No theme selected.\");\n process.exit(1);\n }\n return themes.find((t) => t.id === id)!;\n}\n\nexport function createPullCommand(): Command {\n return new Command(\"pull\")\n .description(\"Pull a remote theme to your local directory\")\n .option(\"-t, --theme <name-or-id>\", \"Theme name or ID to pull\")\n .option(\"-n, --nodelete\", \"Do not delete local files missing on remote\")\n .option(\"--root <path>\", \"Theme root directory\", \".\")\n .action(\n async (opts: { theme?: string; nodelete?: boolean; root: string }) => {\n requireToken();\n\n const api = createApiClient();\n const theme = await selectOrFindTheme(api, opts.theme);\n const themeRoot = new ThemeRoot(opts.root);\n\n const syncer = new Syncer(api, theme.id, themeRoot);\n const spinner = ora(`Pulling ${theme.name} (#${theme.id})…`).start();\n\n const result = await syncer.downloadTheme({\n delete: !opts.nodelete,\n onProgress: (d, total) => {\n spinner.text = `Downloading ${d}/${total} files…`;\n },\n });\n\n if (result.errors.length) {\n spinner.warn(`Pulled with ${result.errors.length} error(s).`);\n for (const e of result.errors) console.error(` ${e}`);\n } else {\n spinner.succeed(\n `Downloaded ${result.downloaded} file(s), deleted ${result.deleted} local file(s).`,\n );\n }\n },\n );\n}\n","import { Command } from \"commander\";\nimport { execFileSync } from \"node:child_process\";\nimport { rmSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport prompts from \"prompts\";\n\nconst DEFAULT_CLONE_URL = \"git@github.com:fluid-commerce/base-theme.git\";\n\nconst SAFE_NAME_RE = /^[a-zA-Z0-9_][a-zA-Z0-9._-]*$/;\n\nexport function createInitCommand(): Command {\n return new Command(\"init\")\n .description(\"Initialize a new theme by cloning the base theme\")\n .argument(\"[name]\", \"Directory name for the new theme\")\n .option(\"-u, --clone-url <url>\", \"Git URL to clone from\", DEFAULT_CLONE_URL)\n .action(async (name: string | undefined, opts: { cloneUrl: string }) => {\n if (!name) {\n const res = await prompts(\n {\n type: \"text\",\n name: \"name\",\n message: \"Theme name\",\n },\n { onCancel: () => process.exit(130) },\n );\n name = res.name as string;\n if (!name) {\n console.error(\"No name provided.\");\n process.exit(1);\n }\n }\n\n if (!SAFE_NAME_RE.test(name)) {\n console.error(\n `Invalid theme name: '${name}'. Use only letters, numbers, hyphens, underscores, and dots.`,\n );\n process.exit(1);\n }\n\n console.log(`Cloning theme from ${opts.cloneUrl} into ${name}…`);\n execFileSync(\"git\", [\"clone\", opts.cloneUrl, name], { stdio: \"inherit\" });\n\n for (const dir of [\".git\", \".github\"]) {\n const path = join(name, dir);\n if (existsSync(path)) rmSync(path, { recursive: true, force: true });\n }\n\n console.log(`\\nTheme initialized in ./${name}`);\n console.log(`Next steps:\\n cd ${name}\\n fluid theme push`);\n });\n}\n","import { Command } from \"commander\";\nimport prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { getPluginState } from \"../plugin-state.js\";\n\nconst STATIC_ROUTES = [\n { label: \"Home\", path: \"/home\" },\n { label: \"Shop\", path: \"/home/shop\" },\n { label: \"Join / Sign Up\", path: \"/home/join\" },\n { label: \"Cart\", path: \"/cart\" },\n { label: \"Blog\", path: \"/home/blog\" },\n { label: \"Categories (all)\", path: \"/home/categories\" },\n { label: \"Collections (all)\", path: \"/home/collections\" },\n] as const;\n\nconst RESOURCE_ROUTES = [\n {\n label: \"Category\",\n type: \"category\",\n template: \"/home/categories/%s\",\n fallback: \"/home/categories\",\n },\n {\n label: \"Collection\",\n type: \"collection\",\n template: \"/home/collections/%s\",\n fallback: \"/home/collections\",\n },\n {\n label: \"Product\",\n type: \"product\",\n template: \"/home/products/%s\",\n fallback: \"/home/shop\",\n },\n {\n label: \"Library\",\n type: \"library\",\n template: \"/home/libraries/%s\",\n fallback: \"/home/libraries\",\n },\n {\n label: \"Post\",\n type: \"post\",\n template: \"/home/posts/%s\",\n fallback: \"/home/blog\",\n },\n {\n label: \"Media\",\n type: \"medium\",\n template: \"/home/media/%s\",\n fallback: \"/home/media\",\n },\n {\n label: \"Enrollment Pack\",\n type: \"enrollment_pack\",\n template: \"/home/enrollments/%s\",\n fallback: \"/home/join\",\n },\n] as const;\n\nexport function createNavigateCommand(): Command {\n return new Command(\"navigate\")\n .description(\"Interactively navigate to a route in the dev server browser\")\n .option(\"--host <host>\", \"Dev server host\", \"127.0.0.1\")\n .option(\"--port <port>\", \"Dev server port\", \"9292\")\n .option(\"-t, --theme <id>\", \"Theme ID (defaults to active dev theme)\")\n .action(async (opts: { host: string; port: string; theme?: string }) => {\n requireToken();\n\n const themeId = opts.theme\n ? Number(opts.theme)\n : getPluginState().devThemeId;\n\n if (!themeId) {\n console.error(\n \"No active dev theme. Run `fluid theme dev` first, or pass --theme <id>.\",\n );\n process.exit(1);\n }\n\n const address = `http://${opts.host}:${opts.port}`;\n\n type Choice = {\n title: string;\n value:\n | string\n | {\n resourceType: string;\n template: string;\n fallback: string;\n label: string;\n };\n };\n const choices: Choice[] = [\n ...STATIC_ROUTES.map((r) => ({ title: r.label, value: r.path })),\n ...RESOURCE_ROUTES.map((r) => ({\n title: `${r.label} (select specific)`,\n value: {\n resourceType: r.type,\n template: r.template,\n fallback: r.fallback,\n label: r.label,\n },\n })),\n ];\n\n const onCancel = () => process.exit(130);\n\n const { dest } = await prompts(\n {\n type: \"select\",\n name: \"dest\",\n message: \"Select a route\",\n choices,\n },\n { onCancel },\n );\n\n if (!dest) return;\n\n let path: string;\n if (typeof dest === \"string\") {\n path = dest;\n } else {\n const api = createApiClient();\n const body = await api.get<{\n available_themeables: Array<{ slug: string; title?: string }>;\n }>(`/api/application_themes/${themeId}/available_themeables`, {\n themeable: dest.resourceType,\n per_page: 50,\n });\n const resources = body.available_themeables ?? [];\n\n if (!resources.length) {\n console.log(`No ${dest.label} resources found, using listing page.`);\n path = dest.fallback;\n } else {\n const { slug } = await prompts(\n {\n type: \"select\",\n name: \"slug\",\n message: `Select a ${dest.label.toLowerCase()}`,\n choices: resources.map((r) => ({\n title: r.title ?? r.slug,\n value: r.slug,\n })),\n },\n { onCancel },\n );\n path = dest.template.replace(\"%s\", slug as string);\n }\n }\n\n const url = `${address}${path}`;\n console.log(`\\nNavigating to: ${url}\\n`);\n const open = (await import(\"open\")).default;\n await open(url);\n });\n}\n","import { Command } from \"commander\";\nimport type { PluginContext } from \"@fluid-app/fluid-cli\";\nimport { createDevCommand } from \"./dev.js\";\nimport { createPushCommand } from \"./push.js\";\nimport { createPullCommand } from \"./pull.js\";\nimport { createInitCommand } from \"./init.js\";\nimport { createNavigateCommand } from \"./navigate.js\";\n\nexport function registerThemeCommand(ctx: PluginContext): void {\n const cmd = new Command(\"theme\").description(\n \"Theme developer workflow β€” dev server, push, pull, init\",\n );\n\n cmd.addCommand(createDevCommand());\n cmd.addCommand(createPushCommand());\n cmd.addCommand(createPullCommand());\n cmd.addCommand(createInitCommand());\n cmd.addCommand(createNavigateCommand());\n\n ctx.program.addCommand(cmd);\n}\n","import type { FluidPlugin, PluginContext } from \"@fluid-app/fluid-cli\";\nimport { registerThemeCommand } from \"./commands/theme.js\";\n\nconst plugin: FluidPlugin = {\n name: \"@fluid-app/fluid-cli-theme-dev\",\n version: \"0.1.0\",\n register(ctx: PluginContext) {\n registerThemeCommand(ctx);\n },\n};\n\nexport default plugin;\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["themes.listThemeResources","themes.updateThemeResource","themes.deleteThemeResource","themes.listApplicationThemes","themes.getApplicationTheme","themes.createApplicationTheme","themes.listApplicationThemes","themes.publishApplicationTheme","themes.listApplicationThemes","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/commands/dev.ts","../src/commands/push.ts","../src/commands/pull.ts","../src/commands/init.ts","../src/commands/navigate.ts","../src/commands/theme.ts","../src/index.ts"],"sourcesContent":["/**\n * Minimal, framework-agnostic fetch client for Fluid APIs\n * Compatible with fluid-admin patterns but usable standalone\n */\n\nexport interface FetchClientConfig {\n /**\n * Base URL for all requests (e.g., \"https://api.fluid.app/api\")\n */\n baseUrl: string;\n\n /**\n * Optional function to get auth token\n * Return null/undefined if no token available\n */\n getAuthToken?: () => string | null | Promise<string | null>;\n\n /**\n * Optional callback when 401 auth error occurs\n */\n onAuthError?: () => void;\n\n /**\n * Default headers to include in all requests\n * Example: { \"x-fluid-client\": \"admin\" }\n */\n defaultHeaders?: Record<string, string>;\n}\n\nexport interface RequestOptions {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\";\n headers?: Record<string, string>;\n params?: Record<string, unknown>;\n body?: unknown;\n signal?: AbortSignal;\n}\n\n/**\n * API Error class compatible with fluid-admin's ApiError\n */\nexport class ApiError extends Error {\n public readonly status: number;\n public readonly data: unknown;\n\n constructor(message: string, status: number, data?: unknown) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.data = data;\n\n if (\"captureStackTrace\" in Error) {\n (\n Error as {\n captureStackTrace: (\n target: Error,\n constructor: NewableFunction,\n ) => void;\n }\n ).captureStackTrace(this, ApiError);\n }\n }\n\n toJSON(): { name: string; message: string; status: number; data: unknown } {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n data: this.data,\n };\n }\n}\n\n/**\n * Type guard for ApiError\n */\nexport function isApiError(error: unknown): error is ApiError {\n return error instanceof ApiError;\n}\n\nexport interface FetchClientInstance {\n request: <TResponse = unknown>(\n endpoint: string,\n options?: RequestOptions,\n ) => Promise<TResponse>;\n requestWithFormData: <TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options?: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n },\n ) => Promise<TResponse>;\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ) => Promise<TResponse>;\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ) => Promise<TResponse>;\n}\n\n/**\n * Creates a configured fetch client instance\n */\nexport function createFetchClient(\n config: FetchClientConfig,\n): FetchClientInstance {\n const { baseUrl, getAuthToken, onAuthError, defaultHeaders = {} } = config;\n\n /**\n * Build headers for a request\n */\n async function buildHeaders(\n customHeaders?: Record<string, string>,\n ): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...defaultHeaders,\n ...customHeaders,\n };\n\n // Add auth token if available\n if (getAuthToken) {\n const token = await getAuthToken();\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n }\n\n return headers;\n }\n\n /**\n * Join baseUrl + endpoint via string concatenation (matches fetchApi).\n * Using `new URL(endpoint, baseUrl)` would strip any path prefix from\n * baseUrl (e.g. \"/api\") when the endpoint starts with \"/\".\n */\n function joinUrl(endpoint: string): string {\n return `${baseUrl}${endpoint}`;\n }\n\n /**\n * Build URL with query parameters for GET requests\n * Compatible with fluid-admin's query param handling\n */\n function buildUrl(\n endpoint: string,\n params?: Record<string, unknown>,\n ): string {\n const fullUrl = joinUrl(endpoint);\n\n if (!params || Object.keys(params).length === 0) {\n return fullUrl;\n }\n\n const queryString = new URLSearchParams();\n\n Object.entries(params).forEach(([key, value]) => {\n if (value === undefined || value === null) {\n return; // Skip undefined/null values\n }\n\n if (Array.isArray(value)) {\n // Handle arrays like Rails expects: key[]\n value.forEach((item) => queryString.append(`${key}[]`, String(item)));\n } else if (typeof value === \"object\") {\n // Handle nested objects: key[subkey]\n Object.entries(value).forEach(([subKey, subValue]) => {\n if (subValue === undefined || subValue === null) {\n return;\n }\n\n if (Array.isArray(subValue)) {\n subValue.forEach((item) =>\n queryString.append(`${key}[${subKey}][]`, String(item)),\n );\n } else {\n queryString.append(`${key}[${subKey}]`, String(subValue));\n }\n });\n } else {\n queryString.append(key, String(value));\n }\n });\n\n const qs = queryString.toString();\n return qs ? `${fullUrl}?${qs}` : fullUrl;\n }\n\n /**\n * Shared response handler for both JSON and FormData requests.\n * Handles auth errors, non-OK responses, 204 No Content, and JSON parsing.\n */\n async function handleResponse<TResponse>(\n response: Response,\n method: string,\n _url: string,\n ): Promise<TResponse> {\n if (response.status === 401 && onAuthError) {\n onAuthError();\n }\n\n if (!response.ok) {\n // Read body as text first to avoid SyntaxError from response.json()\n // when server returns non-JSON bodies with application/json content-type.\n const errorText = await response.text().catch(() => \"\");\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(errorText);\n } catch {\n throw new ApiError(\n errorText.slice(0, 200) ||\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n );\n }\n const msg = (data.message || data.error_message) as string | undefined;\n throw new ApiError(\n msg || `${method} request failed`,\n response.status,\n data.errors || data,\n );\n } else {\n throw new ApiError(\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n );\n }\n }\n\n if (\n response.status === 204 ||\n response.headers.get(\"content-length\") === \"0\"\n ) {\n return null as TResponse;\n }\n\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n try {\n const data = await response.json();\n return data as TResponse;\n } catch {\n try {\n // API declared JSON content-type but body isn't valid JSON\n const text = await response.text();\n return text as TResponse;\n } catch {\n return null as TResponse;\n }\n }\n }\n\n // Non-JSON response (text/plain, text/html, etc.)\n return null as TResponse;\n }\n\n /**\n * Main request function\n */\n async function request<TResponse = unknown>(\n endpoint: string,\n options: RequestOptions = {},\n ): Promise<TResponse> {\n const {\n method = \"GET\",\n headers: customHeaders,\n params,\n body,\n signal,\n } = options;\n\n const url = params ? buildUrl(endpoint, params) : joinUrl(endpoint);\n\n const headers = await buildHeaders(customHeaders);\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers };\n const serializedBody =\n body && method !== \"GET\" ? JSON.stringify(body) : null;\n if (serializedBody) fetchOptions.body = serializedBody;\n if (signal) fetchOptions.signal = signal;\n response = await fetch(url, fetchOptions);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n /**\n * Request with FormData (for file uploads)\n */\n async function requestWithFormData<TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n } = {},\n ): Promise<TResponse> {\n const { method = \"POST\", headers: customHeaders, signal } = options;\n\n const url = joinUrl(endpoint);\n const headers = await buildHeaders(customHeaders);\n\n // Remove Content-Type to let browser set it with boundary\n delete headers[\"Content-Type\"];\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers, body: formData };\n if (signal) fetchOptions.signal = signal;\n response = await fetch(url, fetchOptions);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n // Return client with convenience methods\n return {\n request: request,\n requestWithFormData: requestWithFormData,\n\n // Convenience methods for common HTTP verbs\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"GET\" as const,\n ...(params && { params }),\n }),\n\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"POST\",\n body,\n }),\n\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PUT\",\n body,\n }),\n\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PATCH\",\n body,\n }),\n\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"DELETE\",\n }),\n };\n}\n\nexport type FetchClient = FetchClientInstance;\n","import { createFetchClient } from \"@fluid-app/api-client-core\";\nimport type { FetchClientInstance } from \"@fluid-app/api-client-core\";\nimport { getAuthToken } from \"@fluid-app/fluid-cli\";\n\nexport type ApiClient = FetchClientInstance;\n\nfunction getApiBase(): string {\n return process.env[\"FLUID_API_BASE\"] ?? \"https://api.fluid.app\";\n}\n\nexport function createApiClient(tokenOverride?: string): ApiClient {\n return createFetchClient({\n baseUrl: getApiBase(),\n getAuthToken: () => tokenOverride ?? getAuthToken() ?? null,\n });\n}\n\nexport function requireToken(): string {\n const token = getAuthToken();\n if (!token) {\n console.error(\"Not logged in. Run `fluid login` first.\");\n process.exit(1);\n }\n return token;\n}\n","import { readConfig, updateConfig } from \"@fluid-app/fluid-cli\";\n\ninterface ThemeDevState {\n devThemeId?: number;\n devThemeName?: string;\n}\n\nconst PLUGIN_KEY = \"theme-dev\";\n\nexport function getPluginState(): ThemeDevState {\n const config = readConfig();\n return (config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {};\n}\n\nexport function setPluginState(updates: Partial<ThemeDevState>): void {\n updateConfig((config) => ({\n ...config,\n plugins: {\n ...config.plugins,\n [PLUGIN_KEY]: {\n ...((config.plugins[PLUGIN_KEY] as ThemeDevState) ?? {}),\n ...updates,\n },\n },\n }));\n}\n","const TEXT_TYPES: Record<string, string> = {\n \".liquid\": \"text/x-liquid\",\n \".json\": \"application/json\",\n \".css\": \"text/css\",\n \".js\": \"application/javascript\",\n \".html\": \"text/html\",\n \".txt\": \"text/plain\",\n \".md\": \"text/markdown\",\n \".svg\": \"image/svg+xml\",\n};\n\nconst BINARY_TYPES: Record<string, string> = {\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".ico\": \"image/x-icon\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n \".eot\": \"application/vnd.ms-fontobject\",\n \".otf\": \"font/otf\",\n \".pdf\": \"application/pdf\",\n \".zip\": \"application/zip\",\n \".mp4\": \"video/mp4\",\n \".webm\": \"video/webm\",\n \".mp3\": \"audio/mpeg\",\n \".wav\": \"audio/wav\",\n};\n\nexport interface MimeType {\n name: string;\n isText: boolean;\n}\n\nexport function mimeTypeFor(ext: string): MimeType {\n const text = TEXT_TYPES[ext];\n if (text) return { name: text, isText: true };\n\n const binary = BINARY_TYPES[ext];\n if (binary) return { name: binary, isText: false };\n\n return { name: \"application/octet-stream\", isText: false };\n}\n","import {\n readFileSync,\n writeFileSync,\n mkdirSync,\n existsSync,\n statSync,\n} from \"node:fs\";\nimport { extname, basename, relative, dirname } from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { mimeTypeFor, type MimeType } from \"./mime-type.js\";\n\nexport class ThemeFile {\n readonly absolutePath: string;\n readonly relativePath: string;\n readonly mime: MimeType;\n\n constructor(absolutePath: string, root: string) {\n this.absolutePath = absolutePath;\n this.relativePath = relative(root, absolutePath);\n this.mime = mimeTypeFor(extname(absolutePath).toLowerCase());\n }\n\n get name(): string {\n return basename(this.absolutePath);\n }\n\n get isText(): boolean {\n return this.mime.isText;\n }\n\n get isLiquid(): boolean {\n return this.absolutePath.endsWith(\".liquid\");\n }\n\n get isJson(): boolean {\n return this.absolutePath.endsWith(\".json\");\n }\n\n get exists(): boolean {\n return existsSync(this.absolutePath);\n }\n\n read(): string {\n return readFileSync(this.absolutePath, \"utf-8\");\n }\n\n readBinary(): Buffer {\n return readFileSync(this.absolutePath);\n }\n\n write(content: string | Buffer): void {\n mkdirSync(dirname(this.absolutePath), { recursive: true });\n if (typeof content === \"string\") {\n writeFileSync(this.absolutePath, content, \"utf-8\");\n } else {\n writeFileSync(this.absolutePath, content);\n }\n }\n\n checksum(): string {\n const content = this.isText ? this.read() : this.readBinary();\n return createHash(\"sha256\").update(content).digest(\"hex\");\n }\n\n size(): number {\n return statSync(this.absolutePath).size;\n }\n}\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\n\nconst IGNORE_FILE = \".fluidignore\";\n\ninterface Pattern {\n negated: boolean;\n pattern: string;\n}\n\nexport class FluidIgnore {\n private patterns: Pattern[];\n\n constructor(root: string) {\n this.patterns = this.parse(join(root, IGNORE_FILE));\n }\n\n ignore(relativePath: string): boolean {\n let result = false;\n for (const { negated, pattern } of this.patterns) {\n if (this.match(pattern, relativePath)) {\n result = !negated;\n }\n }\n return result;\n }\n\n private parse(filePath: string): Pattern[] {\n if (!existsSync(filePath)) return [];\n return readFileSync(filePath, \"utf-8\")\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l && !l.startsWith(\"#\"))\n .map((l) => {\n const negated = l.startsWith(\"!\");\n let pattern = negated ? l.slice(1) : l;\n if (pattern.startsWith(\"/\")) pattern = pattern.slice(1);\n return { negated, pattern };\n });\n }\n\n private match(pattern: string, path: string): boolean {\n if (pattern.endsWith(\"/\")) {\n return path.startsWith(pattern) || path === pattern.slice(0, -1);\n }\n if (pattern.includes(\"/\")) {\n return this.fnmatch(pattern, path);\n }\n return this.fnmatch(pattern, path) || this.fnmatch(pattern, basename(path));\n }\n\n private fnmatch(pattern: string, str: string): boolean {\n const re = pattern\n .split(\"**\")\n .map((p) =>\n p\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/\\?/g, \"[^/]\"),\n )\n .join(\".*\");\n return new RegExp(`^${re}$`).test(str);\n }\n}\n","import { readdirSync, statSync } from \"node:fs\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport { ThemeFile } from \"./file.js\";\nimport { FluidIgnore } from \"./fluid-ignore.js\";\n\nconst THEME_MARKERS = [\"templates\", \"assets\", \"config\"];\n\nexport class ThemeRoot {\n readonly root: string;\n readonly ignore: FluidIgnore;\n\n constructor(root: string) {\n this.root = resolve(root);\n this.ignore = new FluidIgnore(this.root);\n }\n\n isValid(): boolean {\n return THEME_MARKERS.some((m) => {\n try {\n return statSync(join(this.root, m)).isDirectory();\n } catch {\n return false;\n }\n });\n }\n\n files(): ThemeFile[] {\n return this.glob(this.root).filter(\n (f) => !this.ignore.ignore(f.relativePath),\n );\n }\n\n file(pathOrFile: string | ThemeFile): ThemeFile {\n if (pathOrFile instanceof ThemeFile) return pathOrFile;\n const abs = isAbsolute(pathOrFile)\n ? pathOrFile\n : join(this.root, pathOrFile);\n return new ThemeFile(abs, this.root);\n }\n\n private glob(dir: string): ThemeFile[] {\n const results: ThemeFile[] = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name.startsWith(\".\")) continue;\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...this.glob(full));\n } else if (entry.isFile()) {\n results.push(new ThemeFile(full, this.root));\n }\n }\n return results;\n }\n}\n","import type { ServerResponse } from \"node:http\";\n\nexport class SSEStream {\n private responses = new Set<ServerResponse>();\n\n add(res: ServerResponse): void {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n \"Access-Control-Allow-Origin\": \"*\",\n });\n res.write(\":\\n\\n\");\n this.responses.add(res);\n res.on(\"close\", () => this.responses.delete(res));\n }\n\n broadcast(data: string): void {\n const payload = `data: ${data}\\n\\n`;\n for (const res of this.responses) {\n try {\n res.write(payload);\n } catch {\n this.responses.delete(res);\n }\n }\n }\n\n close(): void {\n for (const res of this.responses) {\n try {\n res.end();\n } catch {\n // ignore\n }\n }\n this.responses.clear();\n }\n\n get size(): number {\n return this.responses.size;\n }\n}\n","export function buildHotReloadScript(mode: \"full-page\" | \"off\"): string {\n return `\n<script>\n(() => {\n window.__FLUID_CLI_ENV__ = ${JSON.stringify({ mode })};\n\n class HotReload {\n static reloadMode() { return window.__FLUID_CLI_ENV__.mode; }\n static isActive() { return HotReload.reloadMode() !== \"off\"; }\n static setHotReloadCookie(files) {\n const expires = new Date(Date.now() + 3000).toUTCString();\n document.cookie = \\`hot_reload_files=\\${files.join(\",\")};expires=\\${expires};path=/\\`;\n }\n static refresh(files) {\n HotReload.setHotReloadCookie(files);\n console.log(\"[HotReload] Refreshing page\");\n window.location.reload();\n }\n }\n\n class SSEClient {\n constructor(url, handler) {\n if (typeof EventSource === \"undefined\") {\n console.error(\"[HotReload] EventSource not supported in this browser.\");\n return;\n }\n console.log(\"[HotReload] Initializing…\");\n this.url = url;\n this.handler = handler;\n }\n connect() {\n const es = new EventSource(this.url);\n es.onopen = () => console.log(\"[HotReload] SSE connected.\");\n es.onerror = () => {\n console.log(\"[HotReload] SSE closed. Reconnecting in 5s…\");\n es.close();\n setTimeout(() => this.connect(), 5000);\n };\n es.onmessage = (msg) => {\n const data = JSON.parse(msg.data);\n if (data.reload_page) { HotReload.refresh([]); return; }\n this.handler(data);\n };\n }\n }\n\n if (HotReload.isActive()) {\n new SSEClient(\"/hot-reload\", (data) => {\n if (data.modified) HotReload.refresh(data.modified);\n }).connect();\n }\n})();\n</script>`;\n}\n\nexport function injectHotReload(\n html: string,\n mode: \"full-page\" | \"off\",\n): string {\n const script = buildHotReloadScript(mode);\n if (html.includes(\"</body>\")) {\n return html.replace(\"</body>\", `${script}\\n</body>`);\n }\n return html + script;\n}\n","import https from \"node:https\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { injectHotReload } from \"./hot-reload.js\";\nimport { getAuthToken } from \"@fluid-app/fluid-cli\";\n\nconst HOP_BY_HOP = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n \"content-security-policy\",\n]);\n\nexport interface ProxyOptions {\n company: string;\n themeId: number;\n reloadMode: \"full-page\" | \"off\";\n pendingFiles?: () => Array<{ relativePath: string; read: () => string }>;\n}\n\nexport async function proxyRequest(\n req: IncomingMessage,\n res: ServerResponse,\n opts: ProxyOptions,\n): Promise<void> {\n const companyHost = `${opts.company}.fluid.app`;\n\n const headers: Record<string, string> = {};\n for (const [k, v] of Object.entries(req.headers)) {\n if (!HOP_BY_HOP.has(k.toLowerCase()) && typeof v === \"string\") {\n headers[k] = v;\n }\n }\n headers[\"host\"] = companyHost;\n headers[\"x-fluid-theme\"] = String(opts.themeId);\n headers[\"user-agent\"] = \"Fluid CLI\";\n headers[\"accept-encoding\"] = \"identity\";\n\n const url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n url.searchParams.set(\"_fd\", \"0\");\n url.searchParams.set(\"pb\", \"0\");\n\n const pending = opts.pendingFiles?.() ?? [];\n const isGet = req.method === \"GET\" || req.method === \"HEAD\";\n let method = req.method ?? \"GET\";\n let body: string | Buffer | undefined;\n\n if (pending.length > 0 && isGet) {\n method = \"POST\";\n const params = new URLSearchParams();\n params.set(\"_method\", req.method ?? \"GET\");\n for (const f of pending) {\n params.set(`replace_templates[${f.relativePath}]`, f.read());\n }\n const token = getAuthToken();\n if (token) headers[\"authorization\"] = `Bearer ${token}`;\n headers[\"content-type\"] = \"application/x-www-form-urlencoded\";\n body = params.toString();\n headers[\"content-length\"] = String(Buffer.byteLength(body));\n } else if (!isGet) {\n body = await readBody(req);\n if (body.length > 0) {\n headers[\"content-length\"] = String(body.length);\n }\n }\n\n return new Promise((resolve, reject) => {\n const options: https.RequestOptions = {\n hostname: companyHost,\n port: 443,\n path: url.pathname + (url.search || \"\"),\n method,\n headers,\n };\n\n const proxyReq = https.request(options, (proxyRes) => {\n const contentType = proxyRes.headers[\"content-type\"] ?? \"\";\n const isHtml = contentType.includes(\"text/html\");\n\n const responseHeaders: Record<string, string | string[]> = {};\n for (const [k, v] of Object.entries(proxyRes.headers)) {\n if (!HOP_BY_HOP.has(k.toLowerCase()) && v !== undefined) {\n responseHeaders[k] = v as string | string[];\n }\n }\n\n if (isHtml) {\n const chunks: Buffer[] = [];\n proxyRes.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n proxyRes.on(\"end\", () => {\n let html = Buffer.concat(chunks).toString(\"utf-8\");\n html = injectHotReload(html, opts.reloadMode);\n responseHeaders[\"content-length\"] = String(Buffer.byteLength(html));\n res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);\n res.end(html);\n resolve();\n });\n } else {\n res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);\n proxyRes.pipe(res);\n proxyRes.on(\"end\", resolve);\n }\n });\n\n proxyReq.on(\"error\", (err) => {\n reject(err);\n });\n\n if (body) proxyReq.write(body);\n proxyReq.end();\n });\n}\n\nfunction readBody(req: IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n","import { relative } from \"node:path\";\nimport chokidar from \"chokidar\";\nimport type { ThemeRoot } from \"../root.js\";\nimport type { ThemeFile } from \"../file.js\";\n\nexport type FileChangeHandler = (\n modified: ThemeFile[],\n added: ThemeFile[],\n removed: ThemeFile[],\n) => Promise<void>;\n\nexport function watchTheme(\n root: ThemeRoot,\n handler: FileChangeHandler,\n): () => Promise<void> {\n const watcher = chokidar.watch(root.root, {\n ignoreInitial: true,\n ignored: (filePath: string) => {\n if (filePath.includes(\"node_modules\")) return true;\n try {\n const rel = relative(root.root, filePath);\n const basename = rel.split(/[\\\\/]/).pop() ?? \"\";\n return basename.startsWith(\".\") || root.ignore.ignore(rel);\n } catch {\n return false;\n }\n },\n persistent: true,\n awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 },\n });\n\n let pending = Promise.resolve();\n const enqueue = (fn: () => Promise<void>) => {\n pending = pending.then(fn).catch(() => {});\n };\n\n watcher.on(\"change\", (filePath) => {\n const rel = relative(root.root, filePath);\n if (root.ignore.ignore(rel)) return;\n enqueue(() => handler([root.file(filePath)], [], []));\n });\n\n watcher.on(\"add\", (filePath) => {\n const rel = relative(root.root, filePath);\n if (root.ignore.ignore(rel)) return;\n enqueue(() => handler([], [root.file(filePath)], []));\n });\n\n watcher.on(\"unlink\", (filePath) => {\n enqueue(() => handler([], [], [root.file(filePath)]));\n });\n\n return () => watcher.close();\n}\n","/**\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 { 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, type components } from \"@fluid-app/themes-api-client\";\n\ntype ApplicationTheme = components[\"schemas\"][\"ApplicationTheme\"];\n\ninterface CompanyMe {\n data: { company: { subdomain?: string; name?: string } };\n}\n\nasync function ensureDevTheme(\n api: ReturnType<typeof createApiClient>,\n identifier?: string,\n): Promise<ApplicationTheme> {\n if (identifier) {\n const body = await themes.listApplicationThemes(api);\n const found =\n (body.application_themes ?? []).find(\n (t) => String(t.id) === identifier,\n ) ??\n (body.application_themes ?? []).find(\n (t) => t.name.toLowerCase() === identifier.toLowerCase(),\n );\n if (!found) {\n console.error(`Theme not found: ${identifier}`);\n process.exit(1);\n }\n return found;\n }\n\n // Reuse stored dev theme if it still exists\n const { devThemeId } = getPluginState();\n if (devThemeId) {\n try {\n const body = await 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, type components } from \"@fluid-app/themes-api-client\";\n\ntype ApplicationTheme = components[\"schemas\"][\"ApplicationTheme\"];\n\nasync function selectTheme(\n api: ReturnType<typeof createApiClient>,\n): Promise<ApplicationTheme> {\n const body = await themes.listApplicationThemes(api);\n const themeList = body.application_themes ?? [];\n if (!themeList.length) {\n console.error(\"No themes found.\");\n process.exit(1);\n }\n const { id } = await prompts(\n {\n type: \"select\",\n name: \"id\",\n message: \"Select a theme to push to\",\n choices: themeList.map((t) => ({\n title: `${t.name} (#${t.id})`,\n value: t.id,\n })),\n },\n { onCancel: () => process.exit(130) },\n );\n if (!id) {\n console.error(\"No theme selected.\");\n process.exit(1);\n }\n return themeList.find((t) => t.id === id)!;\n}\n\nasync function findTheme(\n api: ReturnType<typeof createApiClient>,\n identifier: string,\n): Promise<ApplicationTheme> {\n const body = await themes.listApplicationThemes(api);\n const themeList = body.application_themes ?? [];\n const found =\n themeList.find((t) => String(t.id) === identifier) ??\n themeList.find((t) => t.name.toLowerCase() === identifier.toLowerCase());\n if (!found) {\n console.error(`No theme found with identifier: ${identifier}`);\n process.exit(1);\n }\n return found;\n}\n\nexport function createPushCommand(): Command {\n return new Command(\"push\")\n .description(\"Push local theme files to a remote theme\")\n .option(\"-t, --theme <name-or-id>\", \"Theme name or ID to push to\")\n .option(\"-n, --nodelete\", \"Do not delete remote files missing locally\")\n .option(\"-f, --force\", \"Skip schema validation\")\n .option(\"-p, --publish\", \"Publish the theme after pushing\")\n .option(\"--root <path>\", \"Theme root directory\", \".\")\n .action(\n async (opts: {\n theme?: string;\n nodelete?: boolean;\n force?: boolean;\n publish?: boolean;\n root: string;\n }) => {\n requireToken();\n\n const themeRoot = new ThemeRoot(opts.root);\n if (!themeRoot.isValid()) {\n console.error(`'${opts.root}' does not look like a theme directory.`);\n process.exit(1);\n }\n\n const api = createApiClient();\n const theme = opts.theme\n ? await findTheme(api, opts.theme)\n : await selectTheme(api);\n\n const syncer = new Syncer(api, theme.id, themeRoot);\n const spinner = ora(`Pushing to ${theme.name} (#${theme.id})…`).start();\n\n const result = await syncer.uploadTheme({\n delete: !opts.nodelete,\n onProgress: (d, total) => {\n spinner.text = `Pushing ${d}/${total} files…`;\n },\n });\n\n if (result.errors.length) {\n spinner.warn(`Pushed with ${result.errors.length} error(s).`);\n for (const e of result.errors) console.error(` ${e}`);\n } else {\n spinner.succeed(\n `Pushed ${result.uploaded} file(s), deleted ${result.deleted} remote file(s).`,\n );\n }\n\n if (opts.publish) {\n const pubSpinner = ora(\"Publishing theme…\").start();\n try {\n await 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 prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { ThemeRoot } from \"../theme/root.js\";\nimport { Syncer } from \"../theme/syncer.js\";\nimport { themes, type components } from \"@fluid-app/themes-api-client\";\n\ntype ApplicationTheme = components[\"schemas\"][\"ApplicationTheme\"];\n\nasync function selectOrFindTheme(\n api: ReturnType<typeof createApiClient>,\n identifier?: string,\n): Promise<ApplicationTheme> {\n const body = await themes.listApplicationThemes(api);\n const themeList = body.application_themes ?? [];\n if (!themeList.length) {\n console.error(\"No themes found.\");\n process.exit(1);\n }\n\n if (identifier) {\n const found =\n themeList.find((t) => String(t.id) === identifier) ??\n themeList.find((t) => t.name.toLowerCase() === identifier.toLowerCase());\n if (!found) {\n console.error(`No theme found with identifier: ${identifier}`);\n process.exit(1);\n }\n return found;\n }\n\n const { id } = await prompts(\n {\n type: \"select\",\n name: \"id\",\n message: \"Select a theme to pull\",\n choices: themeList.map((t) => ({\n title: `${t.name} (#${t.id})`,\n value: t.id,\n })),\n },\n { onCancel: () => process.exit(130) },\n );\n if (!id) {\n console.error(\"No theme selected.\");\n process.exit(1);\n }\n return themeList.find((t) => t.id === id)!;\n}\n\nexport function createPullCommand(): Command {\n return new Command(\"pull\")\n .description(\"Pull a remote theme to your local directory\")\n .option(\"-t, --theme <name-or-id>\", \"Theme name or ID to pull\")\n .option(\"-n, --nodelete\", \"Do not delete local files missing on remote\")\n .option(\"--root <path>\", \"Theme root directory\", \".\")\n .action(\n async (opts: { theme?: string; nodelete?: boolean; root: string }) => {\n requireToken();\n\n const api = createApiClient();\n const theme = await selectOrFindTheme(api, opts.theme);\n const themeRoot = new ThemeRoot(opts.root);\n\n const syncer = new Syncer(api, theme.id, themeRoot);\n const spinner = ora(`Pulling ${theme.name} (#${theme.id})…`).start();\n\n const result = await syncer.downloadTheme({\n delete: !opts.nodelete,\n onProgress: (d, total) => {\n spinner.text = `Downloading ${d}/${total} files…`;\n },\n });\n\n if (result.errors.length) {\n spinner.warn(`Pulled with ${result.errors.length} error(s).`);\n for (const e of result.errors) console.error(` ${e}`);\n } else {\n spinner.succeed(\n `Downloaded ${result.downloaded} file(s), deleted ${result.deleted} local file(s).`,\n );\n }\n },\n );\n}\n","import { Command } from \"commander\";\nimport { execFileSync } from \"node:child_process\";\nimport { rmSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport prompts from \"prompts\";\n\nconst DEFAULT_CLONE_URL = \"git@github.com:fluid-commerce/base-theme.git\";\n\nconst SAFE_NAME_RE = /^[a-zA-Z0-9_][a-zA-Z0-9._-]*$/;\n\nexport function createInitCommand(): Command {\n return new Command(\"init\")\n .description(\"Initialize a new theme by cloning the base theme\")\n .argument(\"[name]\", \"Directory name for the new theme\")\n .option(\"-u, --clone-url <url>\", \"Git URL to clone from\", DEFAULT_CLONE_URL)\n .action(async (name: string | undefined, opts: { cloneUrl: string }) => {\n if (!name) {\n const res = await prompts(\n {\n type: \"text\",\n name: \"name\",\n message: \"Theme name\",\n },\n { onCancel: () => process.exit(130) },\n );\n name = res.name as string;\n if (!name) {\n console.error(\"No name provided.\");\n process.exit(1);\n }\n }\n\n if (!SAFE_NAME_RE.test(name)) {\n console.error(\n `Invalid theme name: '${name}'. Use only letters, numbers, hyphens, underscores, and dots.`,\n );\n process.exit(1);\n }\n\n console.log(`Cloning theme from ${opts.cloneUrl} into ${name}…`);\n execFileSync(\"git\", [\"clone\", opts.cloneUrl, name], { stdio: \"inherit\" });\n\n for (const dir of [\".git\", \".github\"]) {\n const path = join(name, dir);\n if (existsSync(path)) rmSync(path, { recursive: true, force: true });\n }\n\n console.log(`\\nTheme initialized in ./${name}`);\n console.log(`Next steps:\\n cd ${name}\\n fluid theme push`);\n });\n}\n","import { Command } from \"commander\";\nimport prompts from \"prompts\";\nimport { requireToken, createApiClient } from \"../api.js\";\nimport { getPluginState } from \"../plugin-state.js\";\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] 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluid-app/fluid-cli-theme-dev",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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",
@@ -29,8 +29,9 @@
29
29
  "@types/prompts": "^2.4.9",
30
30
  "tsdown": "^0.21.0",
31
31
  "typescript": "^5",
32
+ "@fluid-app/api-client-core": "0.1.0",
32
33
  "@fluid-app/typescript-config": "0.0.0",
33
- "@fluid-app/api-client-core": "0.1.0"
34
+ "@fluid-app/themes-api-client": "0.1.0"
34
35
  },
35
36
  "engines": {
36
37
  "node": ">=18.0.0"
@@ -3,12 +3,9 @@ import { requireToken, createApiClient } from "../api.js";
3
3
  import { getPluginState, setPluginState } from "../plugin-state.js";
4
4
  import { ThemeRoot } from "../theme/root.js";
5
5
  import { startDevServer } from "../theme/dev-server/index.js";
6
+ import { themes, type components } from "@fluid-app/themes-api-client";
6
7
 
7
- interface ApplicationTheme {
8
- id: number;
9
- name: string;
10
- editor_url?: string;
11
- }
8
+ type ApplicationTheme = components["schemas"]["ApplicationTheme"];
12
9
 
13
10
  interface CompanyMe {
14
11
  data: { company: { subdomain?: string; name?: string } };
@@ -19,9 +16,7 @@ async function ensureDevTheme(
19
16
  identifier?: string,
20
17
  ): Promise<ApplicationTheme> {
21
18
  if (identifier) {
22
- const body = await api.get<{ application_themes: ApplicationTheme[] }>(
23
- "/api/application_themes",
24
- );
19
+ const body = await themes.listApplicationThemes(api);
25
20
  const found =
26
21
  (body.application_themes ?? []).find(
27
22
  (t) => String(t.id) === identifier,
@@ -40,9 +35,7 @@ async function ensureDevTheme(
40
35
  const { devThemeId } = getPluginState();
41
36
  if (devThemeId) {
42
37
  try {
43
- const body = await api.get<{ application_theme: ApplicationTheme }>(
44
- `/api/application_themes/${devThemeId}`,
45
- );
38
+ const body = await themes.getApplicationTheme(api, devThemeId);
46
39
  if (body.application_theme) {
47
40
  console.log(`Using existing dev theme #${devThemeId}`);
48
41
  return body.application_theme;
@@ -61,10 +54,9 @@ async function ensureDevTheme(
61
54
  50,
62
55
  );
63
56
 
64
- const body = await api.post<{ application_theme: ApplicationTheme }>(
65
- "/api/application_themes",
66
- { application_theme: { name, role: "development" } },
67
- );
57
+ const body = await themes.createApplicationTheme(api, {
58
+ application_theme: { name, status: "development" },
59
+ });
68
60
  const theme = body.application_theme;
69
61
  setPluginState({ devThemeId: theme.id, devThemeName: theme.name });
70
62
  console.log(`Created dev theme: ${theme.name} (#${theme.id})`);
@@ -141,7 +133,7 @@ export function createDevCommand(): Command {
141
133
  id: theme.id,
142
134
  name: theme.name,
143
135
  company,
144
- editorUrl: theme.editor_url,
136
+ editorUrl: theme.editor_url ?? undefined,
145
137
  },
146
138
  themeRoot,
147
139
  { host: opts.host, port, reloadMode },
@@ -2,6 +2,7 @@ import { Command } from "commander";
2
2
  import prompts from "prompts";
3
3
  import { requireToken, createApiClient } from "../api.js";
4
4
  import { getPluginState } from "../plugin-state.js";
5
+ import { themes } from "@fluid-app/themes-api-client";
5
6
 
6
7
  const STATIC_ROUTES = [
7
8
  { label: "Home", path: "/home" },
@@ -123,12 +124,11 @@ export function createNavigateCommand(): Command {
123
124
  path = dest;
124
125
  } else {
125
126
  const api = createApiClient();
126
- const body = await api.get<{
127
- available_themeables: Array<{ slug: string; title?: string }>;
128
- }>(`/api/application_themes/${themeId}/available_themeables`, {
129
- themeable: dest.resourceType,
130
- per_page: 50,
131
- });
127
+ const body = await themes.getApplicationThemeAvailableThemeables(
128
+ api,
129
+ themeId,
130
+ { themeable: dest.resourceType, per_page: 50 },
131
+ );
132
132
  const resources = body.available_themeables ?? [];
133
133
 
134
134
  if (!resources.length) {
@@ -141,7 +141,7 @@ export function createNavigateCommand(): Command {
141
141
  name: "slug",
142
142
  message: `Select a ${dest.label.toLowerCase()}`,
143
143
  choices: resources.map((r) => ({
144
- title: r.title ?? r.slug,
144
+ title: r.title ?? r.slug ?? "Untitled",
145
145
  value: r.slug,
146
146
  })),
147
147
  },
@@ -4,29 +4,25 @@ import prompts from "prompts";
4
4
  import { requireToken, createApiClient } from "../api.js";
5
5
  import { ThemeRoot } from "../theme/root.js";
6
6
  import { Syncer } from "../theme/syncer.js";
7
+ import { themes, type components } from "@fluid-app/themes-api-client";
7
8
 
8
- interface ApplicationTheme {
9
- id: number;
10
- name: string;
11
- }
9
+ type ApplicationTheme = components["schemas"]["ApplicationTheme"];
12
10
 
13
11
  async function selectOrFindTheme(
14
12
  api: ReturnType<typeof createApiClient>,
15
13
  identifier?: string,
16
14
  ): Promise<ApplicationTheme> {
17
- const body = await api.get<{ application_themes: ApplicationTheme[] }>(
18
- "/api/application_themes",
19
- );
20
- const themes = body.application_themes ?? [];
21
- if (!themes.length) {
15
+ const body = await themes.listApplicationThemes(api);
16
+ const themeList = body.application_themes ?? [];
17
+ if (!themeList.length) {
22
18
  console.error("No themes found.");
23
19
  process.exit(1);
24
20
  }
25
21
 
26
22
  if (identifier) {
27
23
  const found =
28
- themes.find((t) => String(t.id) === identifier) ??
29
- themes.find((t) => t.name.toLowerCase() === identifier.toLowerCase());
24
+ themeList.find((t) => String(t.id) === identifier) ??
25
+ themeList.find((t) => t.name.toLowerCase() === identifier.toLowerCase());
30
26
  if (!found) {
31
27
  console.error(`No theme found with identifier: ${identifier}`);
32
28
  process.exit(1);
@@ -39,7 +35,7 @@ async function selectOrFindTheme(
39
35
  type: "select",
40
36
  name: "id",
41
37
  message: "Select a theme to pull",
42
- choices: themes.map((t) => ({
38
+ choices: themeList.map((t) => ({
43
39
  title: `${t.name} (#${t.id})`,
44
40
  value: t.id,
45
41
  })),
@@ -50,7 +46,7 @@ async function selectOrFindTheme(
50
46
  console.error("No theme selected.");
51
47
  process.exit(1);
52
48
  }
53
- return themes.find((t) => t.id === id)!;
49
+ return themeList.find((t) => t.id === id)!;
54
50
  }
55
51
 
56
52
  export function createPullCommand(): Command {
@@ -4,20 +4,16 @@ import prompts from "prompts";
4
4
  import { requireToken, createApiClient } from "../api.js";
5
5
  import { ThemeRoot } from "../theme/root.js";
6
6
  import { Syncer } from "../theme/syncer.js";
7
+ import { themes, type components } from "@fluid-app/themes-api-client";
7
8
 
8
- interface ApplicationTheme {
9
- id: number;
10
- name: string;
11
- }
9
+ type ApplicationTheme = components["schemas"]["ApplicationTheme"];
12
10
 
13
11
  async function selectTheme(
14
12
  api: ReturnType<typeof createApiClient>,
15
13
  ): Promise<ApplicationTheme> {
16
- const body = await api.get<{ application_themes: ApplicationTheme[] }>(
17
- "/api/application_themes",
18
- );
19
- const themes = body.application_themes ?? [];
20
- if (!themes.length) {
14
+ const body = await themes.listApplicationThemes(api);
15
+ const themeList = body.application_themes ?? [];
16
+ if (!themeList.length) {
21
17
  console.error("No themes found.");
22
18
  process.exit(1);
23
19
  }
@@ -26,7 +22,7 @@ async function selectTheme(
26
22
  type: "select",
27
23
  name: "id",
28
24
  message: "Select a theme to push to",
29
- choices: themes.map((t) => ({
25
+ choices: themeList.map((t) => ({
30
26
  title: `${t.name} (#${t.id})`,
31
27
  value: t.id,
32
28
  })),
@@ -37,20 +33,18 @@ async function selectTheme(
37
33
  console.error("No theme selected.");
38
34
  process.exit(1);
39
35
  }
40
- return themes.find((t) => t.id === id)!;
36
+ return themeList.find((t) => t.id === id)!;
41
37
  }
42
38
 
43
39
  async function findTheme(
44
40
  api: ReturnType<typeof createApiClient>,
45
41
  identifier: string,
46
42
  ): Promise<ApplicationTheme> {
47
- const body = await api.get<{ application_themes: ApplicationTheme[] }>(
48
- "/api/application_themes",
49
- );
50
- const themes = body.application_themes ?? [];
43
+ const body = await themes.listApplicationThemes(api);
44
+ const themeList = body.application_themes ?? [];
51
45
  const found =
52
- themes.find((t) => String(t.id) === identifier) ??
53
- themes.find((t) => t.name.toLowerCase() === identifier.toLowerCase());
46
+ themeList.find((t) => String(t.id) === identifier) ??
47
+ themeList.find((t) => t.name.toLowerCase() === identifier.toLowerCase());
54
48
  if (!found) {
55
49
  console.error(`No theme found with identifier: ${identifier}`);
56
50
  process.exit(1);
@@ -109,7 +103,7 @@ export function createPushCommand(): Command {
109
103
  if (opts.publish) {
110
104
  const pubSpinner = ora("Publishing theme…").start();
111
105
  try {
112
- await api.post(`/api/application_themes/${theme.id}/publish`);
106
+ await themes.publishApplicationTheme(api, theme.id);
113
107
  pubSpinner.succeed("Theme published.");
114
108
  } catch (e) {
115
109
  pubSpinner.fail(`Publish failed: ${e}`);
@@ -2,14 +2,9 @@ import { sep } from "node:path";
2
2
  import type { ApiClient } from "../api.js";
3
3
  import type { ThemeFile } from "./file.js";
4
4
  import type { ThemeRoot } from "./root.js";
5
+ import { themes, type components } from "@fluid-app/themes-api-client";
5
6
 
6
- export interface RemoteResource {
7
- key: string;
8
- checksum: string;
9
- content?: string;
10
- url?: string;
11
- resource_type: string;
12
- }
7
+ type RemoteResource = components["schemas"]["ApplicationThemeResource"];
13
8
 
14
9
  export interface SyncResult {
15
10
  uploaded: number;
@@ -30,15 +25,13 @@ export class Syncer {
30
25
  // ─── Checksum Management ──────────────────────────────────────────────────
31
26
 
32
27
  async fetchChecksums(): Promise<void> {
33
- const body = await this.api.get<{
34
- application_theme_resources: RemoteResource[];
35
- }>(`/api/application_themes/${this.themeId}/resources`);
28
+ const body = await themes.listThemeResources(this.api, this.themeId);
36
29
  this.updateChecksums(body.application_theme_resources ?? []);
37
30
  }
38
31
 
39
32
  private updateChecksums(resources: RemoteResource[]): void {
40
33
  for (const r of resources) {
41
- if (r.key) this.checksums.set(r.key, r.checksum);
34
+ if (r.key && r.checksum) this.checksums.set(r.key, r.checksum);
42
35
  }
43
36
  for (const key of this.checksums.keys()) {
44
37
  if (this.checksums.has(`${key}.liquid`)) this.checksums.delete(key);
@@ -56,23 +49,19 @@ export class Syncer {
56
49
  // ─── Upload ───────────────────────────────────────────────────────────────
57
50
 
58
51
  async uploadFile(file: ThemeFile): Promise<void> {
59
- const path = `/api/application_themes/${this.themeId}/resources`;
60
52
  if (file.isText) {
61
- await this.api.put(path, {
53
+ await themes.updateThemeResource(this.api, this.themeId, {
62
54
  application_theme_resource: {
63
55
  key: file.relativePath,
64
56
  content: file.read(),
65
57
  },
66
58
  });
67
59
  } else {
68
- await this.uploadBinaryFile(file, path);
60
+ await this.uploadBinaryFile(file);
69
61
  }
70
62
  }
71
63
 
72
- private async uploadBinaryFile(
73
- file: ThemeFile,
74
- resourcePath: string,
75
- ): Promise<void> {
64
+ private async uploadBinaryFile(file: ThemeFile): Promise<void> {
76
65
  // Step 1: Create DAM placeholder
77
66
  const placeholderBody = await this.api.post<{
78
67
  asset: { id: number; canonical_path: string };
@@ -147,7 +136,7 @@ export class Syncer {
147
136
  }>("/api/dam/assets/backfill_imagekit", backfillPayload);
148
137
 
149
138
  // Step 5: Associate with theme resource
150
- await this.api.put(resourcePath, {
139
+ await themes.updateThemeResource(this.api, this.themeId, {
151
140
  application_theme_resource: {
152
141
  key: file.relativePath,
153
142
  dam_asset: {
@@ -181,8 +170,8 @@ export class Syncer {
181
170
  // ─── Delete ───────────────────────────────────────────────────────────────
182
171
 
183
172
  async deleteRemoteFile(relativePath: string): Promise<void> {
184
- await this.api.delete(`/api/application_themes/${this.themeId}/resources`, {
185
- body: { application_theme_resource: { key: relativePath } },
173
+ await themes.deleteThemeResource(this.api, this.themeId, {
174
+ application_theme_resource: { key: relativePath },
186
175
  });
187
176
  this.checksums.delete(relativePath);
188
177
  }
@@ -190,11 +179,10 @@ export class Syncer {
190
179
  // ─── Download ─────────────────────────────────────────────────────────────
191
180
 
192
181
  async downloadAll(): Promise<RemoteResource[]> {
193
- const body = await this.api.get<{
194
- application_theme_resources: RemoteResource[];
195
- }>(`/api/application_themes/${this.themeId}/resources`);
196
- this.updateChecksums(body.application_theme_resources ?? []);
197
- return body.application_theme_resources ?? [];
182
+ const body = await themes.listThemeResources(this.api, this.themeId);
183
+ const resources = body.application_theme_resources ?? [];
184
+ this.updateChecksums(resources);
185
+ return resources;
198
186
  }
199
187
 
200
188
  async downloadBinaryAsset(url: string): Promise<Buffer> {
@@ -280,8 +268,15 @@ export class Syncer {
280
268
  if (resource.resource_type === "FileResource" && resource.url) {
281
269
  const buf = await this.downloadBinaryAsset(resource.url);
282
270
  file.write(buf);
283
- } else if (resource.content !== undefined) {
284
- file.write(resource.content);
271
+ } else if (
272
+ resource.content !== undefined &&
273
+ resource.content !== null
274
+ ) {
275
+ const content =
276
+ typeof resource.content === "string"
277
+ ? resource.content
278
+ : JSON.stringify(resource.content);
279
+ file.write(content);
285
280
  }
286
281
  result.downloaded++;
287
282
  } catch (e) {