zen-fs-config 0.3.12 → 0.3.13

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/dist/index.d.mts CHANGED
@@ -242,6 +242,8 @@ declare class ConfigRepo implements IConfigRepo {
242
242
  private disposed;
243
243
  private configCache;
244
244
  constructor(appId: string, nodeId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>);
245
+ /** Full path to this node's directory on the primary backend. */
246
+ get nodePath(): string;
245
247
  load(rawConfig?: string): Promise<void>;
246
248
  getConfig<T = unknown>(path: string): T;
247
249
  setConfig(path: string, data: unknown): void;
@@ -256,7 +258,7 @@ declare class ConfigRepo implements IConfigRepo {
256
258
  resolveConflict(conflictId: string, mergedContent: unknown): Promise<void>;
257
259
  listConflicts(): Promise<ConflictArchive[]>;
258
260
  dispose(): Promise<void>;
259
- setupSync(rules: SyncRule[], backends: BackendDescriptor[], primaryBackendId: string): Promise<void>;
261
+ setupSync(backends: BackendDescriptor[], primaryBackendId: string): Promise<void>;
260
262
  private persistConfig;
261
263
  private reloadConfigCache;
262
264
  private handleConflict;
package/dist/index.d.ts CHANGED
@@ -242,6 +242,8 @@ declare class ConfigRepo implements IConfigRepo {
242
242
  private disposed;
243
243
  private configCache;
244
244
  constructor(appId: string, nodeId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>);
245
+ /** Full path to this node's directory on the primary backend. */
246
+ get nodePath(): string;
245
247
  load(rawConfig?: string): Promise<void>;
246
248
  getConfig<T = unknown>(path: string): T;
247
249
  setConfig(path: string, data: unknown): void;
@@ -256,7 +258,7 @@ declare class ConfigRepo implements IConfigRepo {
256
258
  resolveConflict(conflictId: string, mergedContent: unknown): Promise<void>;
257
259
  listConflicts(): Promise<ConflictArchive[]>;
258
260
  dispose(): Promise<void>;
259
- setupSync(rules: SyncRule[], backends: BackendDescriptor[], primaryBackendId: string): Promise<void>;
261
+ setupSync(backends: BackendDescriptor[], primaryBackendId: string): Promise<void>;
260
262
  private persistConfig;
261
263
  private reloadConfigCache;
262
264
  private handleConflict;
package/dist/index.js CHANGED
@@ -410,227 +410,59 @@ registerBackend("WebStorage", async (options) => {
410
410
  return wrapZenFSFileSystem({ backend: WebStorage, storage });
411
411
  });
412
412
  registerBackend("GitHub", async (options) => {
413
- const token = options.token ?? "";
414
- const owner = options.owner ?? "";
415
- const repo = options.repo ?? "";
416
- const branch = options.branch ?? "main";
417
- const baseUrl = options.baseUrl ?? "https://api.github.com";
418
- if (!owner || !repo) throw new Error('GitHub backend requires "owner" and "repo" options');
419
- const headers = {
420
- "Accept": "application/vnd.github.v3+json",
421
- "User-Agent": "zen-fs-config"
422
- };
423
- if (token) headers["Authorization"] = `Bearer ${token}`;
424
- const apiUrl = (path) => {
425
- const p = path.startsWith("/") ? path.slice(1) : path;
426
- return `${baseUrl}/repos/${owner}/${repo}/contents/${p}?ref=${branch}`;
427
- };
428
- const treeUrl = () => `${baseUrl}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
429
- const ghStat = (item) => ({
430
- isFile: () => item.type === "file",
431
- isDirectory: () => item.type === "dir",
432
- size: item.size ?? 0
413
+ const { Github } = await import("zen-fs-github");
414
+ return wrapZenFSFileSystem({
415
+ backend: Github,
416
+ token: options.token,
417
+ owner: options.owner,
418
+ repo: options.repo,
419
+ branch: options.branch,
420
+ baseUrl: options.baseUrl && options.baseUrl.trim() || void 0
433
421
  });
434
- const fetchJson = async (url) => {
435
- const res = await fetch(url, { headers });
436
- if (!res.ok) throw new Error(`GitHub API ${res.status}: ${url}`);
437
- return res.json();
438
- };
439
- const backend = {
440
- async readFile(path, ...args) {
441
- const data = await fetchJson(apiUrl(path));
442
- if (data.encoding === "base64") {
443
- const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
444
- if (args[0] === "utf-8") return new TextDecoder().decode(raw);
445
- return raw;
446
- }
447
- return data;
448
- },
449
- async writeFile(path, data, options2) {
450
- const message = options2?.message || `Update ${path}`;
451
- const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
452
- const sha = await (async () => {
453
- try {
454
- const d = await fetchJson(apiUrl(path));
455
- return d.sha;
456
- } catch {
457
- return void 0;
458
- }
459
- })();
460
- await fetch(apiUrl(path), {
461
- method: "PUT",
462
- headers,
463
- body: JSON.stringify({ message, content, sha, branch })
464
- });
465
- },
466
- async readdir(path) {
467
- const data = await fetchJson(apiUrl(path));
468
- return data.map((item) => item.name);
469
- },
470
- async stat(path, ...args) {
471
- try {
472
- const data = await fetchJson(apiUrl(path));
473
- if (Array.isArray(data)) {
474
- return { isFile: () => false, isDirectory: () => true, size: 0 };
475
- }
476
- return ghStat(data);
477
- } catch {
478
- throw new Error(`ENOENT: ${path}`);
479
- }
480
- },
481
- async exists(path) {
482
- try {
483
- await fetchJson(apiUrl(path));
484
- return true;
485
- } catch {
486
- return false;
487
- }
488
- },
489
- async mkdir(path, options2) {
490
- const dirPath = path.replace(/\/$/, "");
491
- const keepPath = `${dirPath}/.gitkeep`;
492
- const message = options2?.message || `Create directory ${dirPath}`;
493
- const content = btoa("");
494
- await fetch(apiUrl(keepPath), {
495
- method: "PUT",
496
- headers,
497
- body: JSON.stringify({ message, content, branch })
498
- });
499
- },
500
- async unlink(path) {
501
- const data = await fetchJson(apiUrl(path));
502
- await fetch(apiUrl(path), {
503
- method: "DELETE",
504
- headers,
505
- body: JSON.stringify({ message: `Delete ${path}`, sha: data.sha, branch })
506
- });
507
- },
508
- async rmdir(path) {
509
- const items = await fetchJson(apiUrl(path));
510
- if (Array.isArray(items)) {
511
- for (const item of items) {
512
- const itemPath = `${path}/${item.name}`;
513
- if (item.type === "dir") {
514
- await backend.rmdir(itemPath);
515
- } else {
516
- await backend.unlink(itemPath);
517
- }
518
- }
519
- }
520
- },
521
- async rename(oldPath, newPath) {
522
- const content = await backend.readFile(oldPath);
523
- await backend.writeFile(newPath, content);
524
- await backend.unlink(oldPath);
525
- }
526
- };
527
- return backend;
528
422
  });
529
423
  registerBackend("Gitee", async (options) => {
530
- const token = options.token ?? "";
531
- const owner = options.owner ?? "";
532
- const repo = options.repo ?? "";
533
- const branch = options.branch ?? "master";
534
- const baseUrl = options.baseUrl ?? "https://gitee.com/api/v5";
535
- if (!owner || !repo) throw new Error('Gitee backend requires "owner" and "repo" options');
536
- const fetchJson = async (url) => {
537
- const res = await fetch(url);
538
- if (!res.ok) throw new Error(`Gitee API ${res.status}: ${url}`);
539
- return res.json();
540
- };
541
- const apiUrl = (path) => {
542
- const p = path.startsWith("/") ? path.slice(1) : path;
543
- const params = new URLSearchParams({ access_token: token, ref: branch, path: p });
544
- return `${baseUrl}/repos/${owner}/${repo}/contents?${params}`;
545
- };
546
- const ghStat = (item) => ({
547
- isFile: () => item.type === "file",
548
- isDirectory: () => item.type === "dir",
549
- size: item.size ?? 0
550
- });
551
- const backend = {
552
- async readFile(path, ...args) {
553
- const data = await fetchJson(apiUrl(path));
554
- if (data.content) {
555
- const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
556
- if (args[0] === "utf-8") return new TextDecoder().decode(raw);
557
- return raw;
558
- }
559
- return data;
560
- },
561
- async writeFile(path, data, options2) {
562
- const message = options2?.message || `Update ${path}`;
563
- const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
564
- const sha = await (async () => {
565
- try {
566
- const d = await fetchJson(apiUrl(path));
567
- return d.sha;
568
- } catch {
569
- return void 0;
570
- }
571
- })();
572
- await fetch(apiUrl(path), {
573
- method: "POST",
574
- headers: { "Content-Type": "application/json" },
575
- body: JSON.stringify({ access_token: token, message, content, sha, branch })
576
- });
577
- },
578
- async readdir(path) {
579
- const data = await fetchJson(apiUrl(path));
580
- return Array.isArray(data) ? data.map((i) => i.name) : [];
581
- },
582
- async stat(path) {
583
- try {
584
- const data = await fetchJson(apiUrl(path));
585
- if (Array.isArray(data)) return ghStat({ type: "dir", size: 0 });
586
- return ghStat(data);
587
- } catch {
588
- throw new Error(`ENOENT: ${path}`);
589
- }
590
- },
591
- async exists(path) {
592
- try {
593
- await fetchJson(apiUrl(path));
594
- return true;
595
- } catch {
596
- return false;
597
- }
598
- },
599
- async mkdir(path, options2) {
600
- const dirPath = path.replace(/\/$/, "");
601
- const keepPath = `${dirPath}/.gitkeep`;
602
- const message = options2?.message || `Create directory ${dirPath}`;
603
- await fetch(apiUrl(keepPath), {
604
- method: "POST",
605
- headers: { "Content-Type": "application/json" },
606
- body: JSON.stringify({ access_token: token, message, content: btoa(""), branch })
607
- });
608
- },
609
- async unlink(path) {
610
- const data = await fetchJson(apiUrl(path));
611
- await fetch(apiUrl(path), {
612
- method: "DELETE",
613
- headers: { "Content-Type": "application/json" },
614
- body: JSON.stringify({ access_token: token, message: `Delete ${path}`, sha: data.sha, branch })
615
- });
616
- },
617
- async rmdir(path) {
618
- const items = await fetchJson(apiUrl(path));
619
- if (Array.isArray(items)) {
620
- for (const item of items) {
621
- const itemPath = `${path}/${item.name}`;
622
- if (item.type === "dir") await backend.rmdir(itemPath);
623
- else await backend.unlink(itemPath);
624
- }
424
+ const zenGitee = await import("zen-fs-gitee");
425
+ const { GiteeAPI } = await import("zen-fs-gitee/dist/gitee-api.js");
426
+ const originalGetTree = GiteeAPI.prototype.getTree;
427
+ GiteeAPI.prototype.getTree = async function(recursive = true) {
428
+ try {
429
+ return await originalGetTree.call(this, recursive);
430
+ } catch (err) {
431
+ if (!err.message?.includes("404") && !err.message?.includes("Tree not found")) {
432
+ throw err;
625
433
  }
626
- },
627
- async rename(oldPath, newPath) {
628
- const content = await backend.readFile(oldPath);
629
- await backend.writeFile(newPath, content);
630
- await backend.unlink(oldPath);
631
434
  }
435
+ console.log(`[Gitee] getTree failed with branch="${this.branch}", resolving to SHA...`);
436
+ const baseUrl = this.baseUrl || "https://gitee.com/api/v5";
437
+ const sep = "?";
438
+ const auth = `access_token=${this.token}`;
439
+ const branchUrl = `${baseUrl}/repos/${this.owner}/${this.repo}/branches/${this.branch}${sep}${auth}`;
440
+ const branchRes = await fetch(branchUrl);
441
+ if (!branchRes.ok) throw new Error(`Gitee: branch "${this.branch}" not found (${branchRes.status})`);
442
+ const branchData = await branchRes.json();
443
+ const commitSha = branchData.commit?.sha;
444
+ if (!commitSha) throw new Error(`Gitee: could not get commit SHA for branch "${this.branch}"`);
445
+ const commitUrl = `${baseUrl}/repos/${this.owner}/${this.repo}/git/commits/${commitSha}${sep}${auth}`;
446
+ const commitRes = await fetch(commitUrl);
447
+ if (!commitRes.ok) throw new Error(`Gitee: commit ${commitSha} not found (${commitRes.status})`);
448
+ const commitData = await commitRes.json();
449
+ const treeSha = commitData.tree?.sha;
450
+ if (!treeSha) throw new Error(`Gitee: could not get tree SHA from commit ${commitSha}`);
451
+ console.log(`[Gitee] Resolved branch="${this.branch}" \u2192 commit=${commitSha.slice(0, 8)} \u2192 tree=${treeSha.slice(0, 8)}`);
452
+ const treeUrl = `${baseUrl}/repos/${this.owner}/${this.repo}/git/trees/${treeSha}${sep}recursive=${recursive ? 1 : 0}&${auth}`;
453
+ const treeRes = await fetch(treeUrl);
454
+ if (!treeRes.ok) throw new Error(`Gitee: tree ${treeSha} not found (${treeRes.status})`);
455
+ const treeData = await treeRes.json();
456
+ return treeData.tree || [];
632
457
  };
633
- return backend;
458
+ return wrapZenFSFileSystem({
459
+ backend: zenGitee.Gitee,
460
+ token: options.token,
461
+ owner: options.owner,
462
+ repo: options.repo,
463
+ branch: options.branch,
464
+ baseUrl: options.baseUrl && options.baseUrl.trim() || void 0
465
+ });
634
466
  });
635
467
  registerBackend("WebDAV", async (options) => {
636
468
  const url = options.url ?? "";
@@ -857,6 +689,10 @@ var ConfigRepo = class {
857
689
  this.fs = createChrootFS(cachedFS, `/${appId}`);
858
690
  this.rootFS = createChrootFS(cachedFS, "/");
859
691
  }
692
+ /** Full path to this node's directory on the primary backend. */
693
+ get nodePath() {
694
+ return `/nodes/${this.nodeId}`;
695
+ }
860
696
  // -----------------------------------------------------------------------
861
697
  // IConfigRepo — Load
862
698
  // -----------------------------------------------------------------------
@@ -1075,11 +911,14 @@ var ConfigRepo = class {
1075
911
  // -----------------------------------------------------------------------
1076
912
  // Internal — Setup
1077
913
  // -----------------------------------------------------------------------
1078
- async setupSync(rules, backends, primaryBackendId) {
914
+ async setupSync(backends, primaryBackendId) {
1079
915
  console.log(`[ConfigRepo] setupSync: ${backends.length} backends, primary=${primaryBackendId}`);
1080
- console.log(`[ConfigRepo] setupSync: rules=`, JSON.stringify(rules, null, 2));
1081
916
  for (const desc of backends) {
1082
917
  if (desc.id === primaryBackendId) continue;
918
+ if (desc.enabled === false) {
919
+ console.log(`[ConfigRepo] Skipping disabled replica: ${desc.id}`);
920
+ continue;
921
+ }
1083
922
  console.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
1084
923
  try {
1085
924
  const instance = await createBackend(desc);
@@ -1091,39 +930,23 @@ var ConfigRepo = class {
1091
930
  }
1092
931
  }
1093
932
  console.log(`[ConfigRepo] Available replicas:`, Array.from(this.replicaBackends.keys()));
1094
- for (const rule of rules) {
1095
- if (rule.direction === "none") continue;
1096
- if (!rule.replicas?.length) {
1097
- console.log(`[ConfigRepo] Skipping rule ${rule.prefix}: no replicas`);
1098
- continue;
1099
- }
1100
- const zenSyncDirection = rule.direction === "bi-directional" ? import_zen_fs_sync.SyncDirection.BiDirectional : import_zen_fs_sync.SyncDirection.OneWay;
1101
- for (const replicaId of rule.replicas) {
1102
- if (replicaId === primaryBackendId) continue;
1103
- const replica = this.replicaBackends.get(replicaId);
1104
- if (!replica) {
1105
- console.warn(`[ConfigRepo] Skipping pair ${replicaId}: replica not found (available: ${Array.from(this.replicaBackends.keys()).join(", ")})`);
1106
- continue;
1107
- }
1108
- const pair = this.syncEngine.addPair(
1109
- this.fullFS,
1110
- replica.syncable,
1111
- {
1112
- direction: zenSyncDirection,
1113
- conflictStrategy: rule.conflictStrategy,
1114
- filter: {
1115
- includePrefixes: [rule.prefix]
1116
- }
1117
- },
1118
- "/"
1119
- );
1120
- console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, prefix=${rule.prefix}, dir=${rule.direction}, replica=${replicaId}`);
1121
- const conflictHandler = (event) => {
1122
- this.handleConflict(event, rule);
1123
- };
1124
- this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
1125
- this.syncEngine.watch(pair.pairId);
1126
- }
933
+ for (const [replicaId, replica] of this.replicaBackends.entries()) {
934
+ const pair = this.syncEngine.addPair(
935
+ this.fullFS,
936
+ replica.syncable,
937
+ {
938
+ direction: import_zen_fs_sync.SyncDirection.OneWay,
939
+ conflictStrategy: "source-wins"
940
+ // No filter = sync everything under root
941
+ },
942
+ "/"
943
+ );
944
+ console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, replica=${replicaId}, root=/`);
945
+ const conflictHandler = (event) => {
946
+ this.handleConflict(event, { prefix: "/", direction: "one-way" });
947
+ };
948
+ this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
949
+ this.syncEngine.watch(pair.pairId);
1127
950
  }
1128
951
  console.log(`[ConfigRepo] setupSync complete. Sync statuses:`, this.getSyncStatuses());
1129
952
  }
@@ -1441,7 +1264,6 @@ async function createConfigRepo(appId, options) {
1441
1264
  options.onConflict
1442
1265
  );
1443
1266
  await repo.setupSync(
1444
- syncRulesMeta.rules,
1445
1267
  backendsMeta.backends,
1446
1268
  options.primaryBackendId
1447
1269
  );
package/dist/index.mjs CHANGED
@@ -363,227 +363,59 @@ registerBackend("WebStorage", async (options) => {
363
363
  return wrapZenFSFileSystem({ backend: WebStorage, storage });
364
364
  });
365
365
  registerBackend("GitHub", async (options) => {
366
- const token = options.token ?? "";
367
- const owner = options.owner ?? "";
368
- const repo = options.repo ?? "";
369
- const branch = options.branch ?? "main";
370
- const baseUrl = options.baseUrl ?? "https://api.github.com";
371
- if (!owner || !repo) throw new Error('GitHub backend requires "owner" and "repo" options');
372
- const headers = {
373
- "Accept": "application/vnd.github.v3+json",
374
- "User-Agent": "zen-fs-config"
375
- };
376
- if (token) headers["Authorization"] = `Bearer ${token}`;
377
- const apiUrl = (path) => {
378
- const p = path.startsWith("/") ? path.slice(1) : path;
379
- return `${baseUrl}/repos/${owner}/${repo}/contents/${p}?ref=${branch}`;
380
- };
381
- const treeUrl = () => `${baseUrl}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
382
- const ghStat = (item) => ({
383
- isFile: () => item.type === "file",
384
- isDirectory: () => item.type === "dir",
385
- size: item.size ?? 0
366
+ const { Github } = await import("zen-fs-github");
367
+ return wrapZenFSFileSystem({
368
+ backend: Github,
369
+ token: options.token,
370
+ owner: options.owner,
371
+ repo: options.repo,
372
+ branch: options.branch,
373
+ baseUrl: options.baseUrl && options.baseUrl.trim() || void 0
386
374
  });
387
- const fetchJson = async (url) => {
388
- const res = await fetch(url, { headers });
389
- if (!res.ok) throw new Error(`GitHub API ${res.status}: ${url}`);
390
- return res.json();
391
- };
392
- const backend = {
393
- async readFile(path, ...args) {
394
- const data = await fetchJson(apiUrl(path));
395
- if (data.encoding === "base64") {
396
- const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
397
- if (args[0] === "utf-8") return new TextDecoder().decode(raw);
398
- return raw;
399
- }
400
- return data;
401
- },
402
- async writeFile(path, data, options2) {
403
- const message = options2?.message || `Update ${path}`;
404
- const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
405
- const sha = await (async () => {
406
- try {
407
- const d = await fetchJson(apiUrl(path));
408
- return d.sha;
409
- } catch {
410
- return void 0;
411
- }
412
- })();
413
- await fetch(apiUrl(path), {
414
- method: "PUT",
415
- headers,
416
- body: JSON.stringify({ message, content, sha, branch })
417
- });
418
- },
419
- async readdir(path) {
420
- const data = await fetchJson(apiUrl(path));
421
- return data.map((item) => item.name);
422
- },
423
- async stat(path, ...args) {
424
- try {
425
- const data = await fetchJson(apiUrl(path));
426
- if (Array.isArray(data)) {
427
- return { isFile: () => false, isDirectory: () => true, size: 0 };
428
- }
429
- return ghStat(data);
430
- } catch {
431
- throw new Error(`ENOENT: ${path}`);
432
- }
433
- },
434
- async exists(path) {
435
- try {
436
- await fetchJson(apiUrl(path));
437
- return true;
438
- } catch {
439
- return false;
440
- }
441
- },
442
- async mkdir(path, options2) {
443
- const dirPath = path.replace(/\/$/, "");
444
- const keepPath = `${dirPath}/.gitkeep`;
445
- const message = options2?.message || `Create directory ${dirPath}`;
446
- const content = btoa("");
447
- await fetch(apiUrl(keepPath), {
448
- method: "PUT",
449
- headers,
450
- body: JSON.stringify({ message, content, branch })
451
- });
452
- },
453
- async unlink(path) {
454
- const data = await fetchJson(apiUrl(path));
455
- await fetch(apiUrl(path), {
456
- method: "DELETE",
457
- headers,
458
- body: JSON.stringify({ message: `Delete ${path}`, sha: data.sha, branch })
459
- });
460
- },
461
- async rmdir(path) {
462
- const items = await fetchJson(apiUrl(path));
463
- if (Array.isArray(items)) {
464
- for (const item of items) {
465
- const itemPath = `${path}/${item.name}`;
466
- if (item.type === "dir") {
467
- await backend.rmdir(itemPath);
468
- } else {
469
- await backend.unlink(itemPath);
470
- }
471
- }
472
- }
473
- },
474
- async rename(oldPath, newPath) {
475
- const content = await backend.readFile(oldPath);
476
- await backend.writeFile(newPath, content);
477
- await backend.unlink(oldPath);
478
- }
479
- };
480
- return backend;
481
375
  });
482
376
  registerBackend("Gitee", async (options) => {
483
- const token = options.token ?? "";
484
- const owner = options.owner ?? "";
485
- const repo = options.repo ?? "";
486
- const branch = options.branch ?? "master";
487
- const baseUrl = options.baseUrl ?? "https://gitee.com/api/v5";
488
- if (!owner || !repo) throw new Error('Gitee backend requires "owner" and "repo" options');
489
- const fetchJson = async (url) => {
490
- const res = await fetch(url);
491
- if (!res.ok) throw new Error(`Gitee API ${res.status}: ${url}`);
492
- return res.json();
493
- };
494
- const apiUrl = (path) => {
495
- const p = path.startsWith("/") ? path.slice(1) : path;
496
- const params = new URLSearchParams({ access_token: token, ref: branch, path: p });
497
- return `${baseUrl}/repos/${owner}/${repo}/contents?${params}`;
498
- };
499
- const ghStat = (item) => ({
500
- isFile: () => item.type === "file",
501
- isDirectory: () => item.type === "dir",
502
- size: item.size ?? 0
503
- });
504
- const backend = {
505
- async readFile(path, ...args) {
506
- const data = await fetchJson(apiUrl(path));
507
- if (data.content) {
508
- const raw = Uint8Array.from(atob(data.content), (c) => c.charCodeAt(0));
509
- if (args[0] === "utf-8") return new TextDecoder().decode(raw);
510
- return raw;
511
- }
512
- return data;
513
- },
514
- async writeFile(path, data, options2) {
515
- const message = options2?.message || `Update ${path}`;
516
- const content = typeof data === "string" ? btoa(unescape(encodeURIComponent(data))) : btoa(String.fromCharCode(...new Uint8Array(data)));
517
- const sha = await (async () => {
518
- try {
519
- const d = await fetchJson(apiUrl(path));
520
- return d.sha;
521
- } catch {
522
- return void 0;
523
- }
524
- })();
525
- await fetch(apiUrl(path), {
526
- method: "POST",
527
- headers: { "Content-Type": "application/json" },
528
- body: JSON.stringify({ access_token: token, message, content, sha, branch })
529
- });
530
- },
531
- async readdir(path) {
532
- const data = await fetchJson(apiUrl(path));
533
- return Array.isArray(data) ? data.map((i) => i.name) : [];
534
- },
535
- async stat(path) {
536
- try {
537
- const data = await fetchJson(apiUrl(path));
538
- if (Array.isArray(data)) return ghStat({ type: "dir", size: 0 });
539
- return ghStat(data);
540
- } catch {
541
- throw new Error(`ENOENT: ${path}`);
542
- }
543
- },
544
- async exists(path) {
545
- try {
546
- await fetchJson(apiUrl(path));
547
- return true;
548
- } catch {
549
- return false;
550
- }
551
- },
552
- async mkdir(path, options2) {
553
- const dirPath = path.replace(/\/$/, "");
554
- const keepPath = `${dirPath}/.gitkeep`;
555
- const message = options2?.message || `Create directory ${dirPath}`;
556
- await fetch(apiUrl(keepPath), {
557
- method: "POST",
558
- headers: { "Content-Type": "application/json" },
559
- body: JSON.stringify({ access_token: token, message, content: btoa(""), branch })
560
- });
561
- },
562
- async unlink(path) {
563
- const data = await fetchJson(apiUrl(path));
564
- await fetch(apiUrl(path), {
565
- method: "DELETE",
566
- headers: { "Content-Type": "application/json" },
567
- body: JSON.stringify({ access_token: token, message: `Delete ${path}`, sha: data.sha, branch })
568
- });
569
- },
570
- async rmdir(path) {
571
- const items = await fetchJson(apiUrl(path));
572
- if (Array.isArray(items)) {
573
- for (const item of items) {
574
- const itemPath = `${path}/${item.name}`;
575
- if (item.type === "dir") await backend.rmdir(itemPath);
576
- else await backend.unlink(itemPath);
577
- }
377
+ const zenGitee = await import("zen-fs-gitee");
378
+ const { GiteeAPI } = await import("zen-fs-gitee/dist/gitee-api.js");
379
+ const originalGetTree = GiteeAPI.prototype.getTree;
380
+ GiteeAPI.prototype.getTree = async function(recursive = true) {
381
+ try {
382
+ return await originalGetTree.call(this, recursive);
383
+ } catch (err) {
384
+ if (!err.message?.includes("404") && !err.message?.includes("Tree not found")) {
385
+ throw err;
578
386
  }
579
- },
580
- async rename(oldPath, newPath) {
581
- const content = await backend.readFile(oldPath);
582
- await backend.writeFile(newPath, content);
583
- await backend.unlink(oldPath);
584
387
  }
388
+ console.log(`[Gitee] getTree failed with branch="${this.branch}", resolving to SHA...`);
389
+ const baseUrl = this.baseUrl || "https://gitee.com/api/v5";
390
+ const sep = "?";
391
+ const auth = `access_token=${this.token}`;
392
+ const branchUrl = `${baseUrl}/repos/${this.owner}/${this.repo}/branches/${this.branch}${sep}${auth}`;
393
+ const branchRes = await fetch(branchUrl);
394
+ if (!branchRes.ok) throw new Error(`Gitee: branch "${this.branch}" not found (${branchRes.status})`);
395
+ const branchData = await branchRes.json();
396
+ const commitSha = branchData.commit?.sha;
397
+ if (!commitSha) throw new Error(`Gitee: could not get commit SHA for branch "${this.branch}"`);
398
+ const commitUrl = `${baseUrl}/repos/${this.owner}/${this.repo}/git/commits/${commitSha}${sep}${auth}`;
399
+ const commitRes = await fetch(commitUrl);
400
+ if (!commitRes.ok) throw new Error(`Gitee: commit ${commitSha} not found (${commitRes.status})`);
401
+ const commitData = await commitRes.json();
402
+ const treeSha = commitData.tree?.sha;
403
+ if (!treeSha) throw new Error(`Gitee: could not get tree SHA from commit ${commitSha}`);
404
+ console.log(`[Gitee] Resolved branch="${this.branch}" \u2192 commit=${commitSha.slice(0, 8)} \u2192 tree=${treeSha.slice(0, 8)}`);
405
+ const treeUrl = `${baseUrl}/repos/${this.owner}/${this.repo}/git/trees/${treeSha}${sep}recursive=${recursive ? 1 : 0}&${auth}`;
406
+ const treeRes = await fetch(treeUrl);
407
+ if (!treeRes.ok) throw new Error(`Gitee: tree ${treeSha} not found (${treeRes.status})`);
408
+ const treeData = await treeRes.json();
409
+ return treeData.tree || [];
585
410
  };
586
- return backend;
411
+ return wrapZenFSFileSystem({
412
+ backend: zenGitee.Gitee,
413
+ token: options.token,
414
+ owner: options.owner,
415
+ repo: options.repo,
416
+ branch: options.branch,
417
+ baseUrl: options.baseUrl && options.baseUrl.trim() || void 0
418
+ });
587
419
  });
588
420
  registerBackend("WebDAV", async (options) => {
589
421
  const url = options.url ?? "";
@@ -810,6 +642,10 @@ var ConfigRepo = class {
810
642
  this.fs = createChrootFS(cachedFS, `/${appId}`);
811
643
  this.rootFS = createChrootFS(cachedFS, "/");
812
644
  }
645
+ /** Full path to this node's directory on the primary backend. */
646
+ get nodePath() {
647
+ return `/nodes/${this.nodeId}`;
648
+ }
813
649
  // -----------------------------------------------------------------------
814
650
  // IConfigRepo — Load
815
651
  // -----------------------------------------------------------------------
@@ -1028,11 +864,14 @@ var ConfigRepo = class {
1028
864
  // -----------------------------------------------------------------------
1029
865
  // Internal — Setup
1030
866
  // -----------------------------------------------------------------------
1031
- async setupSync(rules, backends, primaryBackendId) {
867
+ async setupSync(backends, primaryBackendId) {
1032
868
  console.log(`[ConfigRepo] setupSync: ${backends.length} backends, primary=${primaryBackendId}`);
1033
- console.log(`[ConfigRepo] setupSync: rules=`, JSON.stringify(rules, null, 2));
1034
869
  for (const desc of backends) {
1035
870
  if (desc.id === primaryBackendId) continue;
871
+ if (desc.enabled === false) {
872
+ console.log(`[ConfigRepo] Skipping disabled replica: ${desc.id}`);
873
+ continue;
874
+ }
1036
875
  console.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
1037
876
  try {
1038
877
  const instance = await createBackend(desc);
@@ -1044,39 +883,23 @@ var ConfigRepo = class {
1044
883
  }
1045
884
  }
1046
885
  console.log(`[ConfigRepo] Available replicas:`, Array.from(this.replicaBackends.keys()));
1047
- for (const rule of rules) {
1048
- if (rule.direction === "none") continue;
1049
- if (!rule.replicas?.length) {
1050
- console.log(`[ConfigRepo] Skipping rule ${rule.prefix}: no replicas`);
1051
- continue;
1052
- }
1053
- const zenSyncDirection = rule.direction === "bi-directional" ? SyncDirection.BiDirectional : SyncDirection.OneWay;
1054
- for (const replicaId of rule.replicas) {
1055
- if (replicaId === primaryBackendId) continue;
1056
- const replica = this.replicaBackends.get(replicaId);
1057
- if (!replica) {
1058
- console.warn(`[ConfigRepo] Skipping pair ${replicaId}: replica not found (available: ${Array.from(this.replicaBackends.keys()).join(", ")})`);
1059
- continue;
1060
- }
1061
- const pair = this.syncEngine.addPair(
1062
- this.fullFS,
1063
- replica.syncable,
1064
- {
1065
- direction: zenSyncDirection,
1066
- conflictStrategy: rule.conflictStrategy,
1067
- filter: {
1068
- includePrefixes: [rule.prefix]
1069
- }
1070
- },
1071
- "/"
1072
- );
1073
- console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, prefix=${rule.prefix}, dir=${rule.direction}, replica=${replicaId}`);
1074
- const conflictHandler = (event) => {
1075
- this.handleConflict(event, rule);
1076
- };
1077
- this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
1078
- this.syncEngine.watch(pair.pairId);
1079
- }
886
+ for (const [replicaId, replica] of this.replicaBackends.entries()) {
887
+ const pair = this.syncEngine.addPair(
888
+ this.fullFS,
889
+ replica.syncable,
890
+ {
891
+ direction: SyncDirection.OneWay,
892
+ conflictStrategy: "source-wins"
893
+ // No filter = sync everything under root
894
+ },
895
+ "/"
896
+ );
897
+ console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, replica=${replicaId}, root=/`);
898
+ const conflictHandler = (event) => {
899
+ this.handleConflict(event, { prefix: "/", direction: "one-way" });
900
+ };
901
+ this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
902
+ this.syncEngine.watch(pair.pairId);
1080
903
  }
1081
904
  console.log(`[ConfigRepo] setupSync complete. Sync statuses:`, this.getSyncStatuses());
1082
905
  }
@@ -1394,7 +1217,6 @@ async function createConfigRepo(appId, options) {
1394
1217
  options.onConflict
1395
1218
  );
1396
1219
  await repo.setupSync(
1397
- syncRulesMeta.rules,
1398
1220
  backendsMeta.backends,
1399
1221
  options.primaryBackendId
1400
1222
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.3.12",
3
+ "version": "0.3.13",
4
4
  "description": "Distributed config management library built on ZenFS, zen-fs-cache, and zen-fs-sync",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -39,6 +39,8 @@
39
39
  "peerDependencies": {
40
40
  "@zenfs/core": ">=2.3.0",
41
41
  "zen-fs-cache": ">=1.0.0",
42
+ "zen-fs-gitee": ">=1.0.0",
43
+ "zen-fs-github": ">=1.0.0",
42
44
  "zen-fs-sync": ">=0.1.0"
43
45
  },
44
46
  "devDependencies": {
@@ -46,6 +48,8 @@
46
48
  "tsup": "^8.5.1",
47
49
  "typescript": "^5.9.3",
48
50
  "zen-fs-cache": "^1.0.1",
51
+ "zen-fs-gitee": "^1.0.0",
52
+ "zen-fs-github": "^1.0.0",
49
53
  "zen-fs-sync": "^0.1.0"
50
54
  },
51
55
  "dependencies": {