@fluid-app/fluid-cli-theme-dev 0.1.9 → 0.1.11

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.
@@ -46,6 +46,11 @@ export class Syncer {
46
46
  return [...this.checksums.keys()];
47
47
  }
48
48
 
49
+ /** Snapshot of remote checksums (key → sha256). Available after fetchChecksums() or downloadAll(). */
50
+ remoteChecksums(): Record<string, string> {
51
+ return Object.fromEntries(this.checksums);
52
+ }
53
+
49
54
  // ─── Upload ───────────────────────────────────────────────────────────────
50
55
 
51
56
  async uploadFile(file: ThemeFile): Promise<void> {
@@ -242,19 +247,27 @@ export class Syncer {
242
247
  async downloadTheme(
243
248
  opts: {
244
249
  delete?: boolean;
250
+ skip?: Set<string>;
245
251
  onProgress?: (done: number, total: number) => void;
246
252
  } = {},
247
- ): Promise<SyncResult> {
253
+ ): Promise<SyncResult & { skipped: number }> {
248
254
  const resources = await this.downloadAll();
249
- const result: SyncResult = {
255
+ const result: SyncResult & { skipped: number } = {
250
256
  uploaded: 0,
251
257
  deleted: 0,
252
258
  downloaded: 0,
259
+ skipped: 0,
253
260
  errors: [],
254
261
  };
255
262
 
256
263
  let done = 0;
257
264
  for (const resource of resources) {
265
+ if (opts.skip?.has(resource.key)) {
266
+ result.skipped++;
267
+ opts.onProgress?.(++done, resources.length);
268
+ continue;
269
+ }
270
+
258
271
  const file = this.themeRoot.file(resource.key);
259
272
 
260
273
  // Guard against path traversal from malicious API responses
@@ -0,0 +1,34 @@
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
+ }
@@ -0,0 +1,71 @@
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/tsdown.config.ts CHANGED
@@ -5,7 +5,7 @@ export default defineConfig({
5
5
  format: ["esm"],
6
6
  dts: { eager: true },
7
7
  clean: true,
8
- target: "node18",
8
+ target: "node24",
9
9
  deps: {
10
10
  neverBundle: [
11
11
  "@fluid-app/fluid-cli",