@fluid-app/fluid-cli-theme-dev 0.1.2 → 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.
@@ -3,12 +3,12 @@ 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
- company: string;
11
- editor_url?: string;
8
+ type ApplicationTheme = components["schemas"]["ApplicationTheme"];
9
+
10
+ interface CompanyMe {
11
+ data: { company: { subdomain?: string; name?: string } };
12
12
  }
13
13
 
14
14
  async function ensureDevTheme(
@@ -16,9 +16,7 @@ async function ensureDevTheme(
16
16
  identifier?: string,
17
17
  ): Promise<ApplicationTheme> {
18
18
  if (identifier) {
19
- const body = await api.get<{ application_themes: ApplicationTheme[] }>(
20
- "/api/application_themes",
21
- );
19
+ const body = await themes.listApplicationThemes(api);
22
20
  const found =
23
21
  (body.application_themes ?? []).find(
24
22
  (t) => String(t.id) === identifier,
@@ -37,9 +35,7 @@ async function ensureDevTheme(
37
35
  const { devThemeId } = getPluginState();
38
36
  if (devThemeId) {
39
37
  try {
40
- const body = await api.get<{ application_theme: ApplicationTheme }>(
41
- `/api/application_themes/${devThemeId}`,
42
- );
38
+ const body = await themes.getApplicationTheme(api, devThemeId);
43
39
  if (body.application_theme) {
44
40
  console.log(`Using existing dev theme #${devThemeId}`);
45
41
  return body.application_theme;
@@ -58,11 +54,9 @@ async function ensureDevTheme(
58
54
  50,
59
55
  );
60
56
 
61
- const body = await api.post<{ application_theme: ApplicationTheme }>(
62
- "/api/application_themes",
63
- { application_theme: { name, role: "development" } },
64
- );
65
-
57
+ const body = await themes.createApplicationTheme(api, {
58
+ application_theme: { name, status: "development" },
59
+ });
66
60
  const theme = body.application_theme;
67
61
  setPluginState({ devThemeId: theme.id, devThemeName: theme.name });
68
62
  console.log(`Created dev theme: ${theme.name} (#${theme.id})`);
@@ -100,8 +94,28 @@ export function createDevCommand(): Command {
100
94
  process.exit(1);
101
95
  }
102
96
 
97
+ const port = Number(opts.port);
98
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
99
+ console.error(
100
+ `Invalid port: '${opts.port}'. Must be an integer between 1 and 65535.`,
101
+ );
102
+ process.exit(1);
103
+ }
104
+
103
105
  const reloadMode = opts.liveReload === "off" ? "off" : "full-page";
104
106
  const api = createApiClient();
107
+
108
+ const companyRes = await api.get<CompanyMe>(
109
+ "/api/company/v1/companies/me",
110
+ );
111
+ const company = companyRes.data?.company?.subdomain;
112
+ if (!company) {
113
+ console.error(
114
+ "Could not determine company subdomain. Make sure your token is valid.",
115
+ );
116
+ process.exit(1);
117
+ }
118
+
105
119
  const theme = await ensureDevTheme(api, opts.theme);
106
120
 
107
121
  let stop: (() => void) | undefined;
@@ -113,21 +127,13 @@ export function createDevCommand(): Command {
113
127
  process.on("SIGINT", cleanup);
114
128
  process.on("SIGTERM", cleanup);
115
129
 
116
- const port = Number(opts.port);
117
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
118
- console.error(
119
- `Invalid port: '${opts.port}'. Must be an integer between 1 and 65535.`,
120
- );
121
- process.exit(1);
122
- }
123
-
124
130
  stop = await startDevServer(
125
131
  api,
126
132
  {
127
133
  id: theme.id,
128
134
  name: theme.name,
129
- company: theme.company,
130
- editorUrl: theme.editor_url,
135
+ company,
136
+ editorUrl: theme.editor_url ?? undefined,
131
137
  },
132
138
  themeRoot,
133
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,21 +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
- company: string;
12
- }
9
+ type ApplicationTheme = components["schemas"]["ApplicationTheme"];
13
10
 
14
11
  async function selectTheme(
15
12
  api: ReturnType<typeof createApiClient>,
16
13
  ): 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) {
14
+ const body = await themes.listApplicationThemes(api);
15
+ const themeList = body.application_themes ?? [];
16
+ if (!themeList.length) {
22
17
  console.error("No themes found.");
23
18
  process.exit(1);
24
19
  }
@@ -27,7 +22,7 @@ async function selectTheme(
27
22
  type: "select",
28
23
  name: "id",
29
24
  message: "Select a theme to push to",
30
- choices: themes.map((t) => ({
25
+ choices: themeList.map((t) => ({
31
26
  title: `${t.name} (#${t.id})`,
32
27
  value: t.id,
33
28
  })),
@@ -38,20 +33,18 @@ async function selectTheme(
38
33
  console.error("No theme selected.");
39
34
  process.exit(1);
40
35
  }
41
- return themes.find((t) => t.id === id)!;
36
+ return themeList.find((t) => t.id === id)!;
42
37
  }
43
38
 
44
39
  async function findTheme(
45
40
  api: ReturnType<typeof createApiClient>,
46
41
  identifier: string,
47
42
  ): Promise<ApplicationTheme> {
48
- const body = await api.get<{ application_themes: ApplicationTheme[] }>(
49
- "/api/application_themes",
50
- );
51
- const themes = body.application_themes ?? [];
43
+ const body = await themes.listApplicationThemes(api);
44
+ const themeList = body.application_themes ?? [];
52
45
  const found =
53
- themes.find((t) => String(t.id) === identifier) ??
54
- 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());
55
48
  if (!found) {
56
49
  console.error(`No theme found with identifier: ${identifier}`);
57
50
  process.exit(1);
@@ -110,7 +103,7 @@ export function createPushCommand(): Command {
110
103
  if (opts.publish) {
111
104
  const pubSpinner = ora("Publishing theme…").start();
112
105
  try {
113
- await api.post(`/api/application_themes/${theme.id}/publish`);
106
+ await themes.publishApplicationTheme(api, theme.id);
114
107
  pubSpinner.succeed("Theme published.");
115
108
  } catch (e) {
116
109
  pubSpinner.fail(`Publish failed: ${e}`);
package/src/theme/root.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readdirSync, statSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
3
  import { ThemeFile } from "./file.js";
4
4
  import { FluidIgnore } from "./fluid-ignore.js";
5
5
 
@@ -32,7 +32,10 @@ export class ThemeRoot {
32
32
 
33
33
  file(pathOrFile: string | ThemeFile): ThemeFile {
34
34
  if (pathOrFile instanceof ThemeFile) return pathOrFile;
35
- return new ThemeFile(join(this.root, pathOrFile), this.root);
35
+ const abs = isAbsolute(pathOrFile)
36
+ ? pathOrFile
37
+ : join(this.root, pathOrFile);
38
+ return new ThemeFile(abs, this.root);
36
39
  }
37
40
 
38
41
  private glob(dir: string): ThemeFile[] {
@@ -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) {