@fluid-app/fluid-cli-theme-dev 0.1.21 → 0.1.23

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.
Files changed (48) hide show
  1. package/README.md +14 -0
  2. package/dist/index.mjs +155 -2
  3. package/dist/index.mjs.map +1 -1
  4. package/package.json +8 -4
  5. package/.turbo/turbo-build.log +0 -16
  6. package/.turbo/turbo-typecheck.log +0 -4
  7. package/jest.config.cjs +0 -21
  8. package/jest.mocks/fluid-cli.ts +0 -33
  9. package/src/__tests__/plugin-state.test.ts +0 -186
  10. package/src/api.ts +0 -28
  11. package/src/commands/dev.ts +0 -186
  12. package/src/commands/init.ts +0 -51
  13. package/src/commands/lint.ts +0 -186
  14. package/src/commands/navigate.ts +0 -259
  15. package/src/commands/pull.ts +0 -242
  16. package/src/commands/push.ts +0 -220
  17. package/src/commands/theme.ts +0 -23
  18. package/src/index.ts +0 -12
  19. package/src/plugin-state.ts +0 -171
  20. package/src/theme/dev-server/hot-reload.ts +0 -65
  21. package/src/theme/dev-server/index.ts +0 -145
  22. package/src/theme/dev-server/proxy.ts +0 -125
  23. package/src/theme/dev-server/sse.ts +0 -43
  24. package/src/theme/dev-server/watcher.ts +0 -54
  25. package/src/theme/file.ts +0 -104
  26. package/src/theme/fluid-ignore.ts +0 -64
  27. package/src/theme/mime-type.ts +0 -45
  28. package/src/theme/root.ts +0 -54
  29. package/src/theme/syncer.ts +0 -338
  30. package/src/theme-config.ts +0 -34
  31. package/src/theme-picker.ts +0 -164
  32. package/src/workspace.ts +0 -71
  33. package/tsconfig.json +0 -10
  34. package/tsdown.config.ts +0 -19
  35. /package/{skills → dist/skills}/themes-review/SKILL.md +0 -0
  36. /package/{skills → dist/skills}/themes-review/references/blocks-vs-sections.md +0 -0
  37. /package/{skills → dist/skills}/themes-review/references/css-js-hygiene.md +0 -0
  38. /package/{skills → dist/skills}/themes-review/references/dead-code.md +0 -0
  39. /package/{skills → dist/skills}/themes-review/references/dynamism.md +0 -0
  40. /package/{skills → dist/skills}/themes-review/references/editor-attributes.md +0 -0
  41. /package/{skills → dist/skills}/themes-review/references/examples.md +0 -0
  42. /package/{skills → dist/skills}/themes-review/references/fairshare-attributes.md +0 -0
  43. /package/{skills → dist/skills}/themes-review/references/global-settings.md +0 -0
  44. /package/{skills → dist/skills}/themes-review/references/liquid-correctness.md +0 -0
  45. /package/{skills → dist/skills}/themes-review/references/navigation.md +0 -0
  46. /package/{skills → dist/skills}/themes-review/references/performance.md +0 -0
  47. /package/{skills → dist/skills}/themes-review/references/security-accessibility.md +0 -0
  48. /package/{skills → dist/skills}/themes-review/references/setting-types.md +0 -0
@@ -1,338 +0,0 @@
1
- import { sep } from "node:path";
2
- import type { ApiClient } from "../api.js";
3
- import type { ThemeFile } from "./file.js";
4
- import type { ThemeRoot } from "./root.js";
5
- import { themes, type components } from "@fluid-app/themes-api-client";
6
-
7
- type RemoteResource = components["schemas"]["ApplicationThemeResource"];
8
-
9
- export interface SyncResult {
10
- uploaded: number;
11
- downloaded: number;
12
- deleted: number;
13
- errors: string[];
14
- validationFailed: boolean;
15
- }
16
-
17
- export class Syncer {
18
- private checksums = new Map<string, string>();
19
-
20
- constructor(
21
- private api: ApiClient,
22
- private themeId: number,
23
- private themeRoot: ThemeRoot,
24
- ) {}
25
-
26
- // ─── Checksum Management ──────────────────────────────────────────────────
27
-
28
- async fetchChecksums(): Promise<void> {
29
- const body = await themes.listThemeResources(this.api, this.themeId);
30
- this.updateChecksums(body.application_theme_resources ?? []);
31
- }
32
-
33
- private updateChecksums(resources: RemoteResource[]): void {
34
- for (const r of resources) {
35
- if (r.key && r.checksum) this.checksums.set(r.key, r.checksum);
36
- }
37
- for (const key of this.checksums.keys()) {
38
- if (this.checksums.has(`${key}.liquid`)) this.checksums.delete(key);
39
- }
40
- }
41
-
42
- hasChanged(file: ThemeFile): boolean {
43
- return file.checksum() !== this.checksums.get(file.relativePath);
44
- }
45
-
46
- remoteKeys(): string[] {
47
- return [...this.checksums.keys()];
48
- }
49
-
50
- /** Snapshot of remote checksums (key → sha256). Available after fetchChecksums() or downloadAll(). */
51
- remoteChecksums(): Record<string, string> {
52
- return Object.fromEntries(this.checksums);
53
- }
54
-
55
- // ─── Upload ───────────────────────────────────────────────────────────────
56
-
57
- async uploadFile(file: ThemeFile): Promise<void> {
58
- if (file.isText) {
59
- await themes.updateThemeResource(this.api, this.themeId, {
60
- application_theme_resource: {
61
- key: file.relativePath,
62
- content: file.read(),
63
- },
64
- });
65
- } else {
66
- await this.uploadBinaryFile(file);
67
- }
68
- }
69
-
70
- private async uploadBinaryFile(file: ThemeFile): Promise<void> {
71
- // Step 1: Create DAM placeholder
72
- const placeholderBody = await this.api.post<{
73
- asset: { id: number; canonical_path: string };
74
- }>("/api/dam/assets", {
75
- placeholder_asset: {
76
- description: `Uploaded via Fluid CLI: ${file.name}`,
77
- mime_type: file.mime.name,
78
- name: file.name,
79
- },
80
- });
81
- const asset = placeholderBody.asset;
82
-
83
- // Step 2: Get ImageKit auth token
84
- const authBody = await this.api.post<{
85
- token: string;
86
- signature: string;
87
- expire: number;
88
- }>("/api/dam/assets/imagekit_auth", {});
89
-
90
- // Step 3: Upload to ImageKit via multipart
91
- const folder = this.canonicalPathToImageKitFolder(asset.canonical_path);
92
- const formData = new FormData();
93
- const blob = new Blob([file.readBinary() as unknown as ArrayBuffer], {
94
- type: file.mime.name,
95
- });
96
- formData.append("file", blob, file.name);
97
- formData.append("token", authBody.token);
98
- formData.append("signature", authBody.signature);
99
- formData.append("expire", String(authBody.expire));
100
- formData.append("folder", folder);
101
- formData.append("fileName", file.name);
102
- formData.append("publicKey", "public_j7s4Ih9ETh/OCp41mVQH7tlXBdU=");
103
-
104
- const ikResp = await fetch(
105
- "https://upload.imagekit.io/api/v1/files/upload",
106
- {
107
- method: "POST",
108
- body: formData,
109
- },
110
- );
111
- if (!ikResp.ok) throw new Error(`ImageKit upload failed: ${ikResp.status}`);
112
- const ikBody = (await ikResp.json()) as {
113
- fileId: string;
114
- url: string;
115
- thumbnailUrl: string;
116
- size: number;
117
- height?: number;
118
- width?: number;
119
- };
120
-
121
- // Step 4: Backfill DAM asset
122
- const backfillPayload: Record<string, unknown> = {
123
- asset: {
124
- id: asset.id,
125
- imagekit_file_id: ikBody.fileId,
126
- imagekit_url: ikBody.url,
127
- mime_type: file.mime.name,
128
- name: file.name,
129
- file_size: ikBody.size,
130
- expected_path: asset.canonical_path,
131
- },
132
- };
133
- if (ikBody.height)
134
- (backfillPayload["asset"] as Record<string, unknown>)["height"] =
135
- ikBody.height;
136
- if (ikBody.width)
137
- (backfillPayload["asset"] as Record<string, unknown>)["width"] =
138
- ikBody.width;
139
-
140
- const backfillBody = await this.api.post<{
141
- asset: { code: string; default_variant_url: string };
142
- }>("/api/dam/assets/backfill_imagekit", backfillPayload);
143
-
144
- // Step 5: Associate with theme resource
145
- await themes.updateThemeResource(this.api, this.themeId, {
146
- application_theme_resource: {
147
- key: file.relativePath,
148
- dam_asset: {
149
- dam_asset_code: backfillBody.asset.code,
150
- content_type: file.mime.name,
151
- content_size: ikBody.size,
152
- filename: file.name,
153
- handle: backfillBody.asset.code,
154
- url: backfillBody.asset.default_variant_url,
155
- preview_image_url: ikBody.thumbnailUrl,
156
- },
157
- },
158
- });
159
- }
160
-
161
- private canonicalPathToImageKitFolder(canonicalPath: string): string {
162
- const parts = canonicalPath.split(".");
163
- const companyId = parts[0] ?? "unknown";
164
- const category = parts[1] ?? "files";
165
- const assetCode = parts[2] ?? "unknown";
166
- const folderMap: Record<string, string> = {
167
- images: "images",
168
- videos: "videos",
169
- audio: "audio",
170
- documents: "documents",
171
- files: "files",
172
- };
173
- return `${companyId}/${folderMap[category] ?? "files"}/${assetCode}`;
174
- }
175
-
176
- // ─── Delete ───────────────────────────────────────────────────────────────
177
-
178
- async deleteRemoteFile(relativePath: string): Promise<void> {
179
- await themes.deleteThemeResource(this.api, this.themeId, {
180
- application_theme_resource: { key: relativePath },
181
- });
182
- this.checksums.delete(relativePath);
183
- }
184
-
185
- // ─── Download ─────────────────────────────────────────────────────────────
186
-
187
- async downloadAll(): Promise<RemoteResource[]> {
188
- const body = await themes.listThemeResources(this.api, this.themeId);
189
- const resources = body.application_theme_resources ?? [];
190
- this.updateChecksums(resources);
191
- return resources;
192
- }
193
-
194
- async downloadBinaryAsset(url: string): Promise<Buffer> {
195
- const resp = await fetch(url);
196
- if (!resp.ok) throw new Error(`Failed to download asset: ${resp.status}`);
197
- return Buffer.from(await resp.arrayBuffer());
198
- }
199
-
200
- // ─── Full Upload ──────────────────────────────────────────────────────────
201
-
202
- async uploadTheme(
203
- opts: {
204
- delete?: boolean;
205
- validate?: boolean;
206
- onProgress?: (done: number, total: number) => void;
207
- } = {},
208
- ): Promise<SyncResult> {
209
- await this.fetchChecksums();
210
-
211
- const localFiles = this.themeRoot.files();
212
- const result: SyncResult = {
213
- uploaded: 0,
214
- deleted: 0,
215
- downloaded: 0,
216
- errors: [],
217
- validationFailed: false,
218
- };
219
-
220
- // Schema validation pass
221
- if (opts.validate) {
222
- for (const file of localFiles) {
223
- if (!file.isLiquid) continue;
224
- const diagnostics = file.validateSchema();
225
- const errors = diagnostics.filter((d) => d.severity === "error");
226
- for (const d of errors) {
227
- result.errors.push(`${file.relativePath}: ${d.message}`);
228
- }
229
- }
230
- if (result.errors.length > 0) {
231
- result.validationFailed = true;
232
- return result;
233
- }
234
- }
235
-
236
- const toUpload = localFiles.filter((f) => f.exists && this.hasChanged(f));
237
- let done = 0;
238
- for (const file of toUpload) {
239
- try {
240
- await this.uploadFile(file);
241
- result.uploaded++;
242
- } catch (e) {
243
- result.errors.push(`Upload ${file.relativePath}: ${e}`);
244
- }
245
- opts.onProgress?.(++done, toUpload.length);
246
- }
247
-
248
- if (opts.delete) {
249
- const localPaths = new Set(localFiles.map((f) => f.relativePath));
250
- const toDelete = this.remoteKeys().filter((k) => !localPaths.has(k));
251
- for (const key of toDelete) {
252
- try {
253
- await this.deleteRemoteFile(key);
254
- result.deleted++;
255
- } catch (e) {
256
- result.errors.push(`Delete ${key}: ${e}`);
257
- }
258
- }
259
- }
260
-
261
- return result;
262
- }
263
-
264
- // ─── Full Download ────────────────────────────────────────────────────────
265
-
266
- async downloadTheme(
267
- opts: {
268
- delete?: boolean;
269
- skip?: Set<string>;
270
- onProgress?: (done: number, total: number) => void;
271
- } = {},
272
- ): Promise<SyncResult & { skipped: number }> {
273
- const resources = await this.downloadAll();
274
- const result: SyncResult & { skipped: number } = {
275
- uploaded: 0,
276
- deleted: 0,
277
- downloaded: 0,
278
- skipped: 0,
279
- errors: [],
280
- validationFailed: false,
281
- };
282
-
283
- let done = 0;
284
- for (const resource of resources) {
285
- if (opts.skip?.has(resource.key)) {
286
- result.skipped++;
287
- opts.onProgress?.(++done, resources.length);
288
- continue;
289
- }
290
-
291
- const file = this.themeRoot.file(resource.key);
292
-
293
- // Guard against path traversal from malicious API responses
294
- if (!file.absolutePath.startsWith(this.themeRoot.root + sep)) {
295
- result.errors.push(`Download ${resource.key}: path traversal detected`);
296
- opts.onProgress?.(++done, resources.length);
297
- continue;
298
- }
299
-
300
- try {
301
- if (resource.resource_type === "FileResource" && resource.url) {
302
- const buf = await this.downloadBinaryAsset(resource.url);
303
- file.write(buf);
304
- } else if (
305
- resource.content !== undefined &&
306
- resource.content !== null
307
- ) {
308
- const content =
309
- typeof resource.content === "string"
310
- ? resource.content
311
- : JSON.stringify(resource.content);
312
- file.write(content);
313
- }
314
- result.downloaded++;
315
- } catch (e) {
316
- result.errors.push(`Download ${resource.key}: ${e}`);
317
- }
318
- opts.onProgress?.(++done, resources.length);
319
- }
320
-
321
- if (opts.delete) {
322
- const remoteKeys = new Set(resources.map((r) => r.key));
323
- for (const file of this.themeRoot.files()) {
324
- if (!remoteKeys.has(file.relativePath)) {
325
- try {
326
- const { unlinkSync } = await import("node:fs");
327
- unlinkSync(file.absolutePath);
328
- result.deleted++;
329
- } catch {
330
- // ignore
331
- }
332
- }
333
- }
334
- }
335
-
336
- return result;
337
- }
338
- }
@@ -1,34 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
-
4
- export interface ThemeConfig {
5
- themeId: number;
6
- themeName: string;
7
- company: string;
8
- lastPulledAt: string | null;
9
- checksums: Record<string, string>;
10
- }
11
-
12
- const CONFIG_FILE = ".fluid-theme.json";
13
-
14
- function configPath(themeRoot: string): string {
15
- return join(themeRoot, CONFIG_FILE);
16
- }
17
-
18
- /** Read `.fluid-theme.json` from a theme directory, or null if it doesn't exist. */
19
- export function readThemeConfig(themeRoot: string): ThemeConfig | null {
20
- const path = configPath(themeRoot);
21
- if (!existsSync(path)) return null;
22
- try {
23
- const raw = readFileSync(path, "utf-8");
24
- return JSON.parse(raw) as ThemeConfig;
25
- } catch {
26
- return null;
27
- }
28
- }
29
-
30
- /** Write `.fluid-theme.json` to a theme directory. */
31
- export function writeThemeConfig(themeRoot: string, config: ThemeConfig): void {
32
- const path = configPath(themeRoot);
33
- writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf-8");
34
- }
@@ -1,164 +0,0 @@
1
- import chalk from "chalk";
2
- import prompts from "prompts";
3
- import type { createApiClient } from "./api.js";
4
- import { themes, type components } from "@fluid-app/themes-api-client";
5
-
6
- export type ApplicationTheme = components["schemas"]["ApplicationTheme"];
7
-
8
- const PAGE_SIZE = 50;
9
- const LOAD_MORE_VALUE = -1;
10
-
11
- function themeLabel(t: ApplicationTheme): string {
12
- const active = t.status === "active" ? ` ${chalk.green("[active]")}` : "";
13
- return `${t.name} (#${t.id})${active}`;
14
- }
15
-
16
- function themeChoices(
17
- themeList: ApplicationTheme[],
18
- hasMore: boolean,
19
- ): prompts.Choice[] {
20
- const choices: prompts.Choice[] = themeList.map((t) => ({
21
- title: themeLabel(t),
22
- value: t.id,
23
- }));
24
- if (hasMore) {
25
- choices.push({
26
- title: chalk.dim(`── Load more themes ──`),
27
- value: LOAD_MORE_VALUE,
28
- });
29
- }
30
- return choices;
31
- }
32
-
33
- async function fetchThemesPage(
34
- api: ReturnType<typeof createApiClient>,
35
- page: number,
36
- searchQuery?: string,
37
- ): Promise<{
38
- themes: ApplicationTheme[];
39
- hasMore: boolean;
40
- }> {
41
- const body = await themes.listApplicationThemes(api, {
42
- per_page: PAGE_SIZE,
43
- page,
44
- ...(searchQuery ? { search_query: searchQuery } : {}),
45
- });
46
- const list = body.application_themes ?? [];
47
- const totalPages = body.meta?.total_pages ?? 1;
48
- return { themes: list, hasMore: page < totalPages };
49
- }
50
-
51
- export async function selectTheme(
52
- api: ReturnType<typeof createApiClient>,
53
- message: string,
54
- ): Promise<ApplicationTheme> {
55
- const allThemes: ApplicationTheme[] = [];
56
- let page = 1;
57
- let hasMore = true;
58
- let initialIndex = 0;
59
-
60
- // Search cache — persists across suggest calls
61
- let searchQuery = "";
62
- let searchResults: ApplicationTheme[] = [];
63
-
64
- while (true) {
65
- if (hasMore && allThemes.length < page * PAGE_SIZE) {
66
- const result = await fetchThemesPage(api, page);
67
- allThemes.push(...result.themes);
68
- hasMore = result.hasMore;
69
- }
70
-
71
- if (!allThemes.length) {
72
- console.error("No themes found.");
73
- process.exit(1);
74
- }
75
-
76
- const choices = themeChoices(allThemes, hasMore);
77
-
78
- const { id } = await prompts(
79
- {
80
- type: "autocomplete",
81
- name: "id",
82
- message,
83
- initial: initialIndex,
84
- choices,
85
- suggest: async (input: string, choices: prompts.Choice[]) => {
86
- if (!input) {
87
- searchQuery = "";
88
- searchResults = [];
89
- return choices;
90
- }
91
-
92
- if (input !== searchQuery) {
93
- searchQuery = input;
94
- try {
95
- const result = await fetchThemesPage(api, 1, input);
96
- searchResults = result.themes;
97
- } catch {
98
- searchResults = [];
99
- }
100
- }
101
-
102
- return searchResults.map((t) => ({
103
- title: themeLabel(t),
104
- value: t.id,
105
- }));
106
- },
107
- },
108
- { onCancel: () => process.exit(130) },
109
- );
110
-
111
- if (id === LOAD_MORE_VALUE) {
112
- initialIndex = allThemes.length;
113
- page++;
114
- continue;
115
- }
116
-
117
- if (!id) {
118
- console.error("No theme selected.");
119
- process.exit(1);
120
- }
121
-
122
- // Check loaded themes first, then search results
123
- const found =
124
- allThemes.find((t) => t.id === id) ??
125
- searchResults.find((t) => t.id === id);
126
- if (found) return found;
127
-
128
- // Fetch directly by ID as fallback
129
- const body = await themes.getApplicationTheme(api, id);
130
- return body.application_theme;
131
- }
132
- }
133
-
134
- export async function findTheme(
135
- api: ReturnType<typeof createApiClient>,
136
- identifier: string,
137
- ): Promise<ApplicationTheme> {
138
- // Try ID lookup first
139
- const idNum = Number(identifier);
140
- if (Number.isInteger(idNum) && idNum > 0) {
141
- try {
142
- const body = await themes.getApplicationTheme(api, idNum);
143
- if (body.application_theme) return body.application_theme;
144
- } catch {
145
- // Not found by ID, fall through to search
146
- }
147
- }
148
-
149
- // Search by name via API with pagination
150
- let page = 1;
151
- let hasMore = true;
152
- while (hasMore) {
153
- const result = await fetchThemesPage(api, page, identifier);
154
- const found = result.themes.find(
155
- (t) => t.name.toLowerCase() === identifier.toLowerCase(),
156
- );
157
- if (found) return found;
158
- hasMore = result.hasMore;
159
- page++;
160
- }
161
-
162
- console.error(`No theme found with identifier: ${identifier}`);
163
- process.exit(1);
164
- }
package/src/workspace.ts DELETED
@@ -1,71 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { dirname, join, relative, resolve, sep } from "node:path";
3
-
4
- export interface FluidWorkspace {
5
- /** Absolute path to the workspace root (where .fluid-workspace.json lives) */
6
- root: string;
7
- /** Parsed workspace config */
8
- config: WorkspaceConfig;
9
- }
10
-
11
- interface WorkspaceConfig {
12
- type: string;
13
- version: number;
14
- }
15
-
16
- const WORKSPACE_FILE = ".fluid-workspace.json";
17
-
18
- /**
19
- * Walk up from `startDir` looking for `.fluid-workspace.json`.
20
- * Returns the workspace info if found, or `null` if not in a workspace.
21
- */
22
- export function findWorkspace(startDir?: string): FluidWorkspace | null {
23
- let dir = resolve(startDir ?? process.cwd());
24
-
25
- // eslint-disable-next-line no-constant-condition
26
- while (true) {
27
- const candidate = join(dir, WORKSPACE_FILE);
28
- if (existsSync(candidate)) {
29
- try {
30
- const raw = readFileSync(candidate, "utf-8");
31
- const config = JSON.parse(raw) as WorkspaceConfig;
32
- return { root: dir, config };
33
- } catch {
34
- return null;
35
- }
36
- }
37
- const parent = dirname(dir);
38
- if (parent === dir) break; // reached filesystem root
39
- dir = parent;
40
- }
41
-
42
- return null;
43
- }
44
-
45
- /**
46
- * If cwd is already inside `{workspace}/local/{company}/...`, return that
47
- * theme root directory. Otherwise return null.
48
- *
49
- * Examples (workspace root = /code/fluid-theme-dev):
50
- * cwd = /code/fluid-theme-dev/local/acme-co → /code/fluid-theme-dev/local/acme-co
51
- * cwd = /code/fluid-theme-dev/local/acme-co/templates → /code/fluid-theme-dev/local/acme-co
52
- * cwd = /code/fluid-theme-dev → null
53
- * cwd = /code/fluid-theme-dev/local → null
54
- */
55
- export function resolveThemeRootFromCwd(
56
- workspace: FluidWorkspace,
57
- ): string | null {
58
- const cwd = resolve(process.cwd());
59
- const localDir = join(workspace.root, "local");
60
- const rel = relative(localDir, cwd);
61
-
62
- // Not under local/ at all, or exactly at local/
63
- if (rel.startsWith("..") || rel === ".") return null;
64
-
65
- // rel is like "acme-co" or "acme-co/templates/subfolder"
66
- // The theme root is the first segment: local/{company}
67
- const firstSegment = rel.split(sep)[0];
68
- if (!firstSegment) return null;
69
-
70
- return join(localDir, firstSegment);
71
- }
package/tsconfig.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "extends": "@fluid-app/typescript-config/base.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src",
6
- "incremental": false
7
- },
8
- "include": ["src/**/*"],
9
- "exclude": ["node_modules", "dist"]
10
- }
package/tsdown.config.ts DELETED
@@ -1,19 +0,0 @@
1
- import { defineConfig } from "tsdown";
2
-
3
- export default defineConfig({
4
- entry: { index: "src/index.ts" },
5
- format: ["esm"],
6
- dts: { eager: true },
7
- clean: true,
8
- target: "node24",
9
- deps: {
10
- neverBundle: [
11
- "@fluid-app/fluid-cli",
12
- "commander",
13
- "chokidar",
14
- "open",
15
- "ora",
16
- "prompts",
17
- ],
18
- },
19
- });
File without changes