@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.
- package/.turbo/turbo-build.log +7 -9
- package/README.md +100 -0
- package/dist/index.mjs +133 -41
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/commands/dev.ts +32 -26
- package/src/commands/navigate.ts +7 -7
- package/src/commands/pull.ts +9 -13
- package/src/commands/push.ts +12 -19
- package/src/theme/root.ts +5 -2
- package/src/theme/syncer.ts +23 -28
package/src/commands/dev.ts
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
|
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
|
|
62
|
-
"
|
|
63
|
-
|
|
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
|
|
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 },
|
package/src/commands/navigate.ts
CHANGED
|
@@ -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
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
themeable: dest.resourceType,
|
|
130
|
-
|
|
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
|
},
|
package/src/commands/pull.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
18
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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:
|
|
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
|
|
49
|
+
return themeList.find((t) => t.id === id)!;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
export function createPullCommand(): Command {
|
package/src/commands/push.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
18
|
-
|
|
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:
|
|
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
|
|
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
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
|
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
|
-
|
|
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[] {
|
package/src/theme/syncer.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
185
|
-
|
|
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.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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 (
|
|
284
|
-
|
|
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) {
|