@fluid-app/fluid-cli-theme-dev 0.1.3 → 0.1.5
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/dist/index.mjs +144 -35
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/commands/dev.ts +8 -16
- package/src/commands/navigate.ts +13 -7
- package/src/commands/pull.ts +9 -13
- package/src/commands/push.ts +44 -21
- package/src/theme/syncer.ts +23 -28
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" },
|
|
@@ -56,6 +57,12 @@ const RESOURCE_ROUTES = [
|
|
|
56
57
|
template: "/home/enrollments/%s",
|
|
57
58
|
fallback: "/home/join",
|
|
58
59
|
},
|
|
60
|
+
{
|
|
61
|
+
label: "Page",
|
|
62
|
+
type: "page",
|
|
63
|
+
template: "/home/pages/%s",
|
|
64
|
+
fallback: "/home/pages",
|
|
65
|
+
},
|
|
59
66
|
] as const;
|
|
60
67
|
|
|
61
68
|
export function createNavigateCommand(): Command {
|
|
@@ -123,12 +130,11 @@ export function createNavigateCommand(): Command {
|
|
|
123
130
|
path = dest;
|
|
124
131
|
} else {
|
|
125
132
|
const api = createApiClient();
|
|
126
|
-
const body = await
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
themeable: dest.resourceType,
|
|
130
|
-
|
|
131
|
-
});
|
|
133
|
+
const body = await themes.getApplicationThemeAvailableThemeables(
|
|
134
|
+
api,
|
|
135
|
+
themeId,
|
|
136
|
+
{ themeable: dest.resourceType, per_page: 50 },
|
|
137
|
+
);
|
|
132
138
|
const resources = body.available_themeables ?? [];
|
|
133
139
|
|
|
134
140
|
if (!resources.length) {
|
|
@@ -141,7 +147,7 @@ export function createNavigateCommand(): Command {
|
|
|
141
147
|
name: "slug",
|
|
142
148
|
message: `Select a ${dest.label.toLowerCase()}`,
|
|
143
149
|
choices: resources.map((r) => ({
|
|
144
|
-
title: r.title ?? r.slug,
|
|
150
|
+
title: r.title ?? r.slug ?? "Untitled",
|
|
145
151
|
value: r.slug,
|
|
146
152
|
})),
|
|
147
153
|
},
|
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,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
|
-
|
|
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
|
|
17
|
-
|
|
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:
|
|
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
|
|
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
|
|
48
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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);
|
|
@@ -65,6 +59,10 @@ export function createPushCommand(): Command {
|
|
|
65
59
|
.option("-n, --nodelete", "Do not delete remote files missing locally")
|
|
66
60
|
.option("-f, --force", "Skip schema validation")
|
|
67
61
|
.option("-p, --publish", "Publish the theme after pushing")
|
|
62
|
+
.option(
|
|
63
|
+
"-u, --unpublished",
|
|
64
|
+
"Create a new unpublished theme and push to it",
|
|
65
|
+
)
|
|
68
66
|
.option("--root <path>", "Theme root directory", ".")
|
|
69
67
|
.action(
|
|
70
68
|
async (opts: {
|
|
@@ -72,6 +70,7 @@ export function createPushCommand(): Command {
|
|
|
72
70
|
nodelete?: boolean;
|
|
73
71
|
force?: boolean;
|
|
74
72
|
publish?: boolean;
|
|
73
|
+
unpublished?: boolean;
|
|
75
74
|
root: string;
|
|
76
75
|
}) => {
|
|
77
76
|
requireToken();
|
|
@@ -83,9 +82,33 @@ export function createPushCommand(): Command {
|
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
const api = createApiClient();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
let theme: ApplicationTheme;
|
|
86
|
+
|
|
87
|
+
if (opts.unpublished) {
|
|
88
|
+
const { name } = await prompts(
|
|
89
|
+
{
|
|
90
|
+
type: "text",
|
|
91
|
+
name: "name",
|
|
92
|
+
message: "Name for the new theme",
|
|
93
|
+
},
|
|
94
|
+
{ onCancel: () => process.exit(130) },
|
|
95
|
+
);
|
|
96
|
+
if (!name) {
|
|
97
|
+
console.error("Theme name is required.");
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
const body = await themes.createApplicationTheme(api, {
|
|
101
|
+
application_theme: { name, status: "draft" },
|
|
102
|
+
});
|
|
103
|
+
theme = body.application_theme;
|
|
104
|
+
console.log(
|
|
105
|
+
`Created unpublished theme: ${theme.name} (#${theme.id})`,
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
theme = opts.theme
|
|
109
|
+
? await findTheme(api, opts.theme)
|
|
110
|
+
: await selectTheme(api);
|
|
111
|
+
}
|
|
89
112
|
|
|
90
113
|
const syncer = new Syncer(api, theme.id, themeRoot);
|
|
91
114
|
const spinner = ora(`Pushing to ${theme.name} (#${theme.id})…`).start();
|
|
@@ -109,7 +132,7 @@ export function createPushCommand(): Command {
|
|
|
109
132
|
if (opts.publish) {
|
|
110
133
|
const pubSpinner = ora("Publishing theme…").start();
|
|
111
134
|
try {
|
|
112
|
-
await
|
|
135
|
+
await themes.publishApplicationTheme(api, theme.id);
|
|
113
136
|
pubSpinner.succeed("Theme published.");
|
|
114
137
|
} catch (e) {
|
|
115
138
|
pubSpinner.fail(`Publish failed: ${e}`);
|
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) {
|