@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.
- package/.turbo/turbo-build.log +8 -6
- package/dist/index.mjs +360 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/commands/dev.ts +38 -15
- package/src/commands/navigate.ts +104 -8
- package/src/commands/pull.ts +204 -6
- package/src/commands/push.ts +116 -5
- package/src/theme/syncer.ts +15 -2
- package/src/theme-config.ts +34 -0
- package/src/workspace.ts +71 -0
- package/tsdown.config.ts +1 -1
package/src/theme/syncer.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/workspace.ts
ADDED
|
@@ -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
|
+
}
|