zen-fs-config 0.3.17 → 0.3.19

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
@@ -279,10 +279,17 @@ declare function createConfigRepo(appId: string, options: ConfigRepoOptions): Pr
279
279
  * zen-fs-config — Backend Registry
280
280
  *
281
281
  * A pluggable registry that maps backend type names to factory functions.
282
- * Built-in support for ZenFS backends (InMemory, IndexedDB, etc.)
283
- * loaded from @zenfs/core.
284
282
  *
285
- * Users can register custom backends via `registerBackend()`.
283
+ * Core principle: zen-fs-config does NOT hardcode every ZenFS backend.
284
+ * Instead, it provides:
285
+ * 1. A simple registry API (registerBackend, createBackend, etc.)
286
+ * 2. One built-in backend (InMemory) — zero extra dependencies
287
+ * 3. A wrapZenFSFileSystem() helper to adapt any ZenFS FileSystem
288
+ * implementation into the BackendInstance interface
289
+ *
290
+ * Applications (like zen-fs-config-admin) register whatever backends
291
+ * they need at startup. Adding a new backend never requires changing
292
+ * zen-fs-config itself.
286
293
  */
287
294
 
288
295
  type BackendFactory = (options: Record<string, unknown>) => Promise<BackendInstance>;
@@ -304,9 +311,11 @@ interface BackendInstance {
304
311
  getRevision?(path: string): Promise<string | number | undefined>;
305
312
  }
306
313
  declare function registerBackend(type: string, factory: BackendFactory): void;
314
+ declare function unregisterBackend(type: string): boolean;
307
315
  declare function createBackend(descriptor: Pick<BackendDescriptor, 'type' | 'options'>): Promise<BackendInstance>;
308
316
  declare function hasBackend(type: string): boolean;
309
317
  declare function listBackends(): string[];
318
+ declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
310
319
 
311
320
  /**
312
321
  * zen-fs-config — Sidecar Version File Management
@@ -350,4 +359,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
350
359
  */
351
360
  declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
352
361
 
353
- export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type BootstrapData, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type IConfigRepo, type SyncRule, type SyncRulesMeta, type VersionMeta, configKeyToFilePath, createBackend, createConfigRepo, createSerializerChain, getExtension, hasBackend, incrementVersion, listBackends, readVersion, registerBackend, sha256, verifyOrRepairVersion, versionPathFor, writeVersion };
362
+ export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type BootstrapData, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type IConfigRepo, type SyncRule, type SyncRulesMeta, type VersionMeta, configKeyToFilePath, createBackend, createConfigRepo, createSerializerChain, getExtension, hasBackend, incrementVersion, listBackends, readVersion, registerBackend, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
package/dist/index.d.ts CHANGED
@@ -279,10 +279,17 @@ declare function createConfigRepo(appId: string, options: ConfigRepoOptions): Pr
279
279
  * zen-fs-config — Backend Registry
280
280
  *
281
281
  * A pluggable registry that maps backend type names to factory functions.
282
- * Built-in support for ZenFS backends (InMemory, IndexedDB, etc.)
283
- * loaded from @zenfs/core.
284
282
  *
285
- * Users can register custom backends via `registerBackend()`.
283
+ * Core principle: zen-fs-config does NOT hardcode every ZenFS backend.
284
+ * Instead, it provides:
285
+ * 1. A simple registry API (registerBackend, createBackend, etc.)
286
+ * 2. One built-in backend (InMemory) — zero extra dependencies
287
+ * 3. A wrapZenFSFileSystem() helper to adapt any ZenFS FileSystem
288
+ * implementation into the BackendInstance interface
289
+ *
290
+ * Applications (like zen-fs-config-admin) register whatever backends
291
+ * they need at startup. Adding a new backend never requires changing
292
+ * zen-fs-config itself.
286
293
  */
287
294
 
288
295
  type BackendFactory = (options: Record<string, unknown>) => Promise<BackendInstance>;
@@ -304,9 +311,11 @@ interface BackendInstance {
304
311
  getRevision?(path: string): Promise<string | number | undefined>;
305
312
  }
306
313
  declare function registerBackend(type: string, factory: BackendFactory): void;
314
+ declare function unregisterBackend(type: string): boolean;
307
315
  declare function createBackend(descriptor: Pick<BackendDescriptor, 'type' | 'options'>): Promise<BackendInstance>;
308
316
  declare function hasBackend(type: string): boolean;
309
317
  declare function listBackends(): string[];
318
+ declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
310
319
 
311
320
  /**
312
321
  * zen-fs-config — Sidecar Version File Management
@@ -350,4 +359,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
350
359
  */
351
360
  declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
352
361
 
353
- export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type BootstrapData, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type IConfigRepo, type SyncRule, type SyncRulesMeta, type VersionMeta, configKeyToFilePath, createBackend, createConfigRepo, createSerializerChain, getExtension, hasBackend, incrementVersion, listBackends, readVersion, registerBackend, sha256, verifyOrRepairVersion, versionPathFor, writeVersion };
362
+ export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type BootstrapData, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type IConfigRepo, type SyncRule, type SyncRulesMeta, type VersionMeta, configKeyToFilePath, createBackend, createConfigRepo, createSerializerChain, getExtension, hasBackend, incrementVersion, listBackends, readVersion, registerBackend, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
package/dist/index.js CHANGED
@@ -42,8 +42,10 @@ __export(index_exports, {
42
42
  readVersion: () => readVersion,
43
43
  registerBackend: () => registerBackend,
44
44
  sha256: () => sha256,
45
+ unregisterBackend: () => unregisterBackend,
45
46
  verifyOrRepairVersion: () => verifyOrRepairVersion,
46
47
  versionPathFor: () => versionPathFor,
48
+ wrapZenFSFileSystem: () => wrapZenFSFileSystem,
47
49
  writeVersion: () => writeVersion
48
50
  });
49
51
  module.exports = __toCommonJS(index_exports);
@@ -311,6 +313,9 @@ var registry = /* @__PURE__ */ new Map();
311
313
  function registerBackend(type, factory) {
312
314
  registry.set(type, factory);
313
315
  }
316
+ function unregisterBackend(type) {
317
+ return registry.delete(type);
318
+ }
314
319
  async function createBackend(descriptor) {
315
320
  const factory = registry.get(descriptor.type);
316
321
  if (!factory) {
@@ -392,236 +397,6 @@ registerBackend("InMemory", async (options) => {
392
397
  const label = options.label ?? `zen-fs-config-${++inMemoryCounter}`;
393
398
  return wrapZenFSFileSystem({ backend: InMemory, maxSize, label });
394
399
  });
395
- var idbCounter = 0;
396
- registerBackend("IndexedDB", async (options) => {
397
- const { IndexedDB } = await import("@zenfs/dom");
398
- const storeName = options.storeName ?? `zen-fs-config-${++idbCounter}`;
399
- return wrapZenFSFileSystem({ backend: IndexedDB, storeName });
400
- });
401
- registerBackend("WebStorage", async (options) => {
402
- const { WebStorage } = await import("@zenfs/dom");
403
- const storageType = options.storageType ?? "localStorage";
404
- let storage;
405
- if (storageType === "sessionStorage" && typeof sessionStorage !== "undefined") {
406
- storage = sessionStorage;
407
- } else {
408
- storage = localStorage;
409
- }
410
- return wrapZenFSFileSystem({ backend: WebStorage, storage });
411
- });
412
- registerBackend("GitHub", async (options) => {
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
421
- });
422
- });
423
- registerBackend("Gitee", async (options) => {
424
- const zenGitee = await import("zen-fs-gitee");
425
- const { GiteeFS } = zenGitee;
426
- const origInit = GiteeFS.prototype.init;
427
- const patched = /* @__PURE__ */ new WeakSet();
428
- GiteeFS.prototype.init = async function() {
429
- let firstErr;
430
- try {
431
- return await origInit.call(this);
432
- } catch (err) {
433
- if (!err.message?.includes("404") && !err.message?.includes("Tree not found")) {
434
- throw err;
435
- }
436
- firstErr = err;
437
- }
438
- if (patched.has(this)) throw firstErr;
439
- patched.add(this);
440
- console.log(`[Gitee] init failed with branch="${this.api.branch}", resolving to SHA...`);
441
- const baseUrl = this.api.baseUrl || "https://gitee.com/api/v5";
442
- const auth = `access_token=${this.api.token}`;
443
- const branchUrl = `${baseUrl}/repos/${this.api.owner}/${this.api.repo}/branches/${this.api.branch}?${auth}`;
444
- let branchRes = await fetch(branchUrl);
445
- let branchData;
446
- if (branchRes.ok) {
447
- branchData = await branchRes.json();
448
- } else {
449
- console.log(`[Gitee] Branch "${this.api.branch}" not found, creating...`);
450
- const defaultBranch = this.api.branch === "master" ? "main" : "master";
451
- let defaultSha;
452
- for (const name of [defaultBranch, "main", "master"]) {
453
- const dr = await fetch(`${baseUrl}/repos/${this.api.owner}/${this.api.repo}/branches/${name}?${auth}`);
454
- if (dr.ok) {
455
- const dd = await dr.json();
456
- defaultSha = dd.commit?.sha;
457
- if (defaultSha) break;
458
- }
459
- }
460
- if (!defaultSha) {
461
- console.log(`[Gitee] No branches found in repo, skipping tree init`);
462
- this.initialized = true;
463
- return;
464
- }
465
- const createRes = await fetch(`${baseUrl}/repos/${this.api.owner}/${this.api.repo}/branches?${auth}`, {
466
- method: "POST",
467
- headers: { "Content-Type": "application/json" },
468
- body: JSON.stringify({
469
- refs: defaultSha,
470
- branch_name: this.api.branch
471
- })
472
- });
473
- if (!createRes.ok) {
474
- const errText = await createRes.text().catch(() => "");
475
- throw new Error(`Gitee: failed to create branch "${this.api.branch}": ${createRes.status} ${errText}`);
476
- }
477
- branchData = await createRes.json();
478
- console.log(`[Gitee] Branch "${this.api.branch}" created from ${defaultSha.slice(0, 8)}`);
479
- }
480
- const commitSha = branchData.commit?.sha;
481
- if (!commitSha) throw new Error(`Gitee: could not get commit SHA for branch "${this.api.branch}"`);
482
- const commitUrl = `${baseUrl}/repos/${this.api.owner}/${this.api.repo}/git/commits/${commitSha}?${auth}`;
483
- const commitRes = await fetch(commitUrl);
484
- if (!commitRes.ok) throw new Error(`Gitee: commit ${commitSha} not found (${commitRes.status})`);
485
- const commitData = await commitRes.json();
486
- const treeSha = commitData.tree?.sha;
487
- if (!treeSha) throw new Error(`Gitee: could not get tree SHA from commit ${commitSha}`);
488
- const realBranch = this.api.branch;
489
- this.api.branch = treeSha;
490
- console.log(`[Gitee] Resolved branch="${realBranch}" \u2192 commit=${commitSha.slice(0, 8)} \u2192 tree=${treeSha.slice(0, 8)}`);
491
- try {
492
- return await origInit.call(this);
493
- } finally {
494
- this.api.branch = realBranch;
495
- }
496
- };
497
- return wrapZenFSFileSystem({
498
- backend: zenGitee.Gitee,
499
- token: options.token,
500
- owner: options.owner,
501
- repo: options.repo,
502
- branch: options.branch,
503
- baseUrl: options.baseUrl && options.baseUrl.trim() || void 0
504
- });
505
- });
506
- registerBackend("WebDAV", async (options) => {
507
- const url = options.url ?? "";
508
- const username = options.username ?? "";
509
- const password = options.password ?? "";
510
- const rootPath = options.rootPath ?? "/";
511
- if (!url) throw new Error('WebDAV backend requires "url" option');
512
- const authHeader = username ? `Basic ${btoa(`${username}:${password}`)}` : "";
513
- const davUrl = (path) => {
514
- const cleanRoot = rootPath.replace(/\/$/, "");
515
- const cleanPath = path.startsWith("/") ? path : `/${path}`;
516
- return `${url.replace(/\/$/, "")}${cleanRoot}${cleanPath}`;
517
- };
518
- const davFetch = async (path, method, body) => {
519
- const headers = {};
520
- if (authHeader) headers["Authorization"] = authHeader;
521
- if (body) headers["Content-Type"] = "application/xml";
522
- const res = await fetch(davUrl(path), { method, headers, body });
523
- if (!res.ok && res.status !== 404) throw new Error(`WebDAV ${res.status} ${method} ${davUrl(path)}`);
524
- return res;
525
- };
526
- const parseMultiStatus = async (res) => {
527
- const text = await res.text();
528
- const results = [];
529
- const responses = text.match(/<D:response[^>]*>[\s\S]*?<\/D:response>/gi) || [];
530
- for (const resp of responses) {
531
- const href = (resp.match(/<D:href>([^<]+)<\/D:href>/i) || [])[1] || "";
532
- const isDir = /<D:collection\s*\/>/i.test(resp) || /<D:resourcetype>.*<D:collection/.test(resp);
533
- const sizeMatch = resp.match(/<D:getcontentlength>([^<]+)<\/D:getcontentlength>/i);
534
- const size = sizeMatch ? parseInt(sizeMatch[1]) : 0;
535
- const decoded = decodeURIComponent(href);
536
- results.push({ path: decoded, isDir, size });
537
- }
538
- return results;
539
- };
540
- const exists = async (path) => {
541
- const res = await davFetch(path, "PROPFIND");
542
- return res.ok;
543
- };
544
- const backend = {
545
- async readFile(path, ...args) {
546
- const res = await davFetch(path, "GET");
547
- if (!res.ok) throw new Error(`ENOENT: ${path}`);
548
- if (args[0] === "utf-8") return res.text();
549
- const buf = await res.arrayBuffer();
550
- return new Uint8Array(buf);
551
- },
552
- async writeFile(path, data, _options) {
553
- const headers = { "Content-Type": "application/octet-stream" };
554
- if (authHeader) headers["Authorization"] = authHeader;
555
- await fetch(davUrl(path), {
556
- method: "PUT",
557
- headers,
558
- body: data instanceof ArrayBuffer ? data : data instanceof Uint8Array ? new Uint8Array(data).buffer : new TextEncoder().encode(data)
559
- });
560
- },
561
- async readdir(path) {
562
- const headers = { Depth: "1" };
563
- if (authHeader) headers["Authorization"] = authHeader;
564
- const res = await fetch(davUrl(path), { method: "PROPFIND", headers });
565
- if (!res.ok) throw new Error(`WebDAV PROPFIND failed: ${res.status}`);
566
- const items = await parseMultiStatus(res);
567
- const prefix = davUrl(path);
568
- return items.filter((i) => i.path !== prefix && i.path !== `${prefix}/`).map((i) => i.path.split("/").filter(Boolean).pop() || "");
569
- },
570
- async stat(path) {
571
- const headers = { Depth: "0" };
572
- if (authHeader) headers["Authorization"] = authHeader;
573
- const res = await fetch(davUrl(path), { method: "PROPFIND", headers });
574
- if (!res.ok) throw new Error(`ENOENT: ${path}`);
575
- const items = await parseMultiStatus(res);
576
- const item = items[0];
577
- return { isFile: () => !item.isDir, isDirectory: () => item.isDir, size: item.size };
578
- },
579
- async exists(path) {
580
- return exists(path);
581
- },
582
- async mkdir(path) {
583
- const headers = {};
584
- if (authHeader) headers["Authorization"] = authHeader;
585
- const res = await fetch(davUrl(path), { method: "MKCOL", headers });
586
- if (!res.ok && res.status !== 405) throw new Error(`WebDAV MKCOL failed: ${res.status}`);
587
- },
588
- async unlink(path) {
589
- await davFetch(path, "DELETE");
590
- },
591
- async rmdir(path) {
592
- const items = await (async () => {
593
- const headers = { Depth: "1" };
594
- if (authHeader) headers["Authorization"] = authHeader;
595
- const res = await fetch(davUrl(path), { method: "PROPFIND", headers });
596
- if (!res.ok) return [];
597
- const parsed = await parseMultiStatus(res);
598
- const prefix = davUrl(path);
599
- return parsed.filter((i) => i.path !== prefix && i.path !== `${prefix}/`);
600
- })();
601
- for (const item of items) {
602
- if (item.isDir) await backend.rmdir(item.path);
603
- else await backend.unlink(item.path);
604
- }
605
- await davFetch(path, "DELETE");
606
- },
607
- async rename(oldPath, newPath) {
608
- const headers = { Destination: davUrl(newPath) };
609
- if (authHeader) headers["Authorization"] = authHeader;
610
- await fetch(davUrl(oldPath), { method: "MOVE", headers });
611
- }
612
- };
613
- return backend;
614
- });
615
- registerBackend("RemoteStorage", async (options) => {
616
- const { RemoteStorageFileSystem } = await import("zen-fs-remotestoragejs");
617
- const href = options.href ?? "";
618
- const token = options.token ?? "";
619
- if (!href) throw new Error('RemoteStorage backend requires "href" option');
620
- if (!token) throw new Error('RemoteStorage backend requires "token" option');
621
- const basePath = options.basePath || void 0;
622
- const fs = new RemoteStorageFileSystem({ href, token, basePath });
623
- return wrapZenFSFileSystem(fs);
624
- });
625
400
 
626
401
  // src/version.ts
627
402
  function versionPathFor(configFilePath) {
@@ -639,9 +414,12 @@ async function sha256(data) {
639
414
  const hex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
640
415
  return `sha256:${hex}`;
641
416
  }
642
- const nodeCrypto = await import("crypto");
643
- const hash = nodeCrypto.createHash("sha256").update(Buffer.from(buffer)).digest("hex");
644
- return `sha256:${hash}`;
417
+ if (typeof globalThis.window === "undefined") {
418
+ const nodeCrypto = await new Function("return import('node:crypto')")();
419
+ const hash = nodeCrypto.createHash("sha256").update(Buffer.from(buffer)).digest("hex");
420
+ return `sha256:${hash}`;
421
+ }
422
+ throw new Error("SHA-256 not available: neither Web Crypto nor Node.js crypto module found");
645
423
  }
646
424
  async function readVersion(fs, versionFilePath) {
647
425
  try {
@@ -1333,7 +1111,9 @@ async function createConfigRepo(appId, options) {
1333
1111
  readVersion,
1334
1112
  registerBackend,
1335
1113
  sha256,
1114
+ unregisterBackend,
1336
1115
  verifyOrRepairVersion,
1337
1116
  versionPathFor,
1117
+ wrapZenFSFileSystem,
1338
1118
  writeVersion
1339
1119
  });
package/dist/index.mjs CHANGED
@@ -264,6 +264,9 @@ var registry = /* @__PURE__ */ new Map();
264
264
  function registerBackend(type, factory) {
265
265
  registry.set(type, factory);
266
266
  }
267
+ function unregisterBackend(type) {
268
+ return registry.delete(type);
269
+ }
267
270
  async function createBackend(descriptor) {
268
271
  const factory = registry.get(descriptor.type);
269
272
  if (!factory) {
@@ -345,236 +348,6 @@ registerBackend("InMemory", async (options) => {
345
348
  const label = options.label ?? `zen-fs-config-${++inMemoryCounter}`;
346
349
  return wrapZenFSFileSystem({ backend: InMemory, maxSize, label });
347
350
  });
348
- var idbCounter = 0;
349
- registerBackend("IndexedDB", async (options) => {
350
- const { IndexedDB } = await import("@zenfs/dom");
351
- const storeName = options.storeName ?? `zen-fs-config-${++idbCounter}`;
352
- return wrapZenFSFileSystem({ backend: IndexedDB, storeName });
353
- });
354
- registerBackend("WebStorage", async (options) => {
355
- const { WebStorage } = await import("@zenfs/dom");
356
- const storageType = options.storageType ?? "localStorage";
357
- let storage;
358
- if (storageType === "sessionStorage" && typeof sessionStorage !== "undefined") {
359
- storage = sessionStorage;
360
- } else {
361
- storage = localStorage;
362
- }
363
- return wrapZenFSFileSystem({ backend: WebStorage, storage });
364
- });
365
- registerBackend("GitHub", async (options) => {
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
374
- });
375
- });
376
- registerBackend("Gitee", async (options) => {
377
- const zenGitee = await import("zen-fs-gitee");
378
- const { GiteeFS } = zenGitee;
379
- const origInit = GiteeFS.prototype.init;
380
- const patched = /* @__PURE__ */ new WeakSet();
381
- GiteeFS.prototype.init = async function() {
382
- let firstErr;
383
- try {
384
- return await origInit.call(this);
385
- } catch (err) {
386
- if (!err.message?.includes("404") && !err.message?.includes("Tree not found")) {
387
- throw err;
388
- }
389
- firstErr = err;
390
- }
391
- if (patched.has(this)) throw firstErr;
392
- patched.add(this);
393
- console.log(`[Gitee] init failed with branch="${this.api.branch}", resolving to SHA...`);
394
- const baseUrl = this.api.baseUrl || "https://gitee.com/api/v5";
395
- const auth = `access_token=${this.api.token}`;
396
- const branchUrl = `${baseUrl}/repos/${this.api.owner}/${this.api.repo}/branches/${this.api.branch}?${auth}`;
397
- let branchRes = await fetch(branchUrl);
398
- let branchData;
399
- if (branchRes.ok) {
400
- branchData = await branchRes.json();
401
- } else {
402
- console.log(`[Gitee] Branch "${this.api.branch}" not found, creating...`);
403
- const defaultBranch = this.api.branch === "master" ? "main" : "master";
404
- let defaultSha;
405
- for (const name of [defaultBranch, "main", "master"]) {
406
- const dr = await fetch(`${baseUrl}/repos/${this.api.owner}/${this.api.repo}/branches/${name}?${auth}`);
407
- if (dr.ok) {
408
- const dd = await dr.json();
409
- defaultSha = dd.commit?.sha;
410
- if (defaultSha) break;
411
- }
412
- }
413
- if (!defaultSha) {
414
- console.log(`[Gitee] No branches found in repo, skipping tree init`);
415
- this.initialized = true;
416
- return;
417
- }
418
- const createRes = await fetch(`${baseUrl}/repos/${this.api.owner}/${this.api.repo}/branches?${auth}`, {
419
- method: "POST",
420
- headers: { "Content-Type": "application/json" },
421
- body: JSON.stringify({
422
- refs: defaultSha,
423
- branch_name: this.api.branch
424
- })
425
- });
426
- if (!createRes.ok) {
427
- const errText = await createRes.text().catch(() => "");
428
- throw new Error(`Gitee: failed to create branch "${this.api.branch}": ${createRes.status} ${errText}`);
429
- }
430
- branchData = await createRes.json();
431
- console.log(`[Gitee] Branch "${this.api.branch}" created from ${defaultSha.slice(0, 8)}`);
432
- }
433
- const commitSha = branchData.commit?.sha;
434
- if (!commitSha) throw new Error(`Gitee: could not get commit SHA for branch "${this.api.branch}"`);
435
- const commitUrl = `${baseUrl}/repos/${this.api.owner}/${this.api.repo}/git/commits/${commitSha}?${auth}`;
436
- const commitRes = await fetch(commitUrl);
437
- if (!commitRes.ok) throw new Error(`Gitee: commit ${commitSha} not found (${commitRes.status})`);
438
- const commitData = await commitRes.json();
439
- const treeSha = commitData.tree?.sha;
440
- if (!treeSha) throw new Error(`Gitee: could not get tree SHA from commit ${commitSha}`);
441
- const realBranch = this.api.branch;
442
- this.api.branch = treeSha;
443
- console.log(`[Gitee] Resolved branch="${realBranch}" \u2192 commit=${commitSha.slice(0, 8)} \u2192 tree=${treeSha.slice(0, 8)}`);
444
- try {
445
- return await origInit.call(this);
446
- } finally {
447
- this.api.branch = realBranch;
448
- }
449
- };
450
- return wrapZenFSFileSystem({
451
- backend: zenGitee.Gitee,
452
- token: options.token,
453
- owner: options.owner,
454
- repo: options.repo,
455
- branch: options.branch,
456
- baseUrl: options.baseUrl && options.baseUrl.trim() || void 0
457
- });
458
- });
459
- registerBackend("WebDAV", async (options) => {
460
- const url = options.url ?? "";
461
- const username = options.username ?? "";
462
- const password = options.password ?? "";
463
- const rootPath = options.rootPath ?? "/";
464
- if (!url) throw new Error('WebDAV backend requires "url" option');
465
- const authHeader = username ? `Basic ${btoa(`${username}:${password}`)}` : "";
466
- const davUrl = (path) => {
467
- const cleanRoot = rootPath.replace(/\/$/, "");
468
- const cleanPath = path.startsWith("/") ? path : `/${path}`;
469
- return `${url.replace(/\/$/, "")}${cleanRoot}${cleanPath}`;
470
- };
471
- const davFetch = async (path, method, body) => {
472
- const headers = {};
473
- if (authHeader) headers["Authorization"] = authHeader;
474
- if (body) headers["Content-Type"] = "application/xml";
475
- const res = await fetch(davUrl(path), { method, headers, body });
476
- if (!res.ok && res.status !== 404) throw new Error(`WebDAV ${res.status} ${method} ${davUrl(path)}`);
477
- return res;
478
- };
479
- const parseMultiStatus = async (res) => {
480
- const text = await res.text();
481
- const results = [];
482
- const responses = text.match(/<D:response[^>]*>[\s\S]*?<\/D:response>/gi) || [];
483
- for (const resp of responses) {
484
- const href = (resp.match(/<D:href>([^<]+)<\/D:href>/i) || [])[1] || "";
485
- const isDir = /<D:collection\s*\/>/i.test(resp) || /<D:resourcetype>.*<D:collection/.test(resp);
486
- const sizeMatch = resp.match(/<D:getcontentlength>([^<]+)<\/D:getcontentlength>/i);
487
- const size = sizeMatch ? parseInt(sizeMatch[1]) : 0;
488
- const decoded = decodeURIComponent(href);
489
- results.push({ path: decoded, isDir, size });
490
- }
491
- return results;
492
- };
493
- const exists = async (path) => {
494
- const res = await davFetch(path, "PROPFIND");
495
- return res.ok;
496
- };
497
- const backend = {
498
- async readFile(path, ...args) {
499
- const res = await davFetch(path, "GET");
500
- if (!res.ok) throw new Error(`ENOENT: ${path}`);
501
- if (args[0] === "utf-8") return res.text();
502
- const buf = await res.arrayBuffer();
503
- return new Uint8Array(buf);
504
- },
505
- async writeFile(path, data, _options) {
506
- const headers = { "Content-Type": "application/octet-stream" };
507
- if (authHeader) headers["Authorization"] = authHeader;
508
- await fetch(davUrl(path), {
509
- method: "PUT",
510
- headers,
511
- body: data instanceof ArrayBuffer ? data : data instanceof Uint8Array ? new Uint8Array(data).buffer : new TextEncoder().encode(data)
512
- });
513
- },
514
- async readdir(path) {
515
- const headers = { Depth: "1" };
516
- if (authHeader) headers["Authorization"] = authHeader;
517
- const res = await fetch(davUrl(path), { method: "PROPFIND", headers });
518
- if (!res.ok) throw new Error(`WebDAV PROPFIND failed: ${res.status}`);
519
- const items = await parseMultiStatus(res);
520
- const prefix = davUrl(path);
521
- return items.filter((i) => i.path !== prefix && i.path !== `${prefix}/`).map((i) => i.path.split("/").filter(Boolean).pop() || "");
522
- },
523
- async stat(path) {
524
- const headers = { Depth: "0" };
525
- if (authHeader) headers["Authorization"] = authHeader;
526
- const res = await fetch(davUrl(path), { method: "PROPFIND", headers });
527
- if (!res.ok) throw new Error(`ENOENT: ${path}`);
528
- const items = await parseMultiStatus(res);
529
- const item = items[0];
530
- return { isFile: () => !item.isDir, isDirectory: () => item.isDir, size: item.size };
531
- },
532
- async exists(path) {
533
- return exists(path);
534
- },
535
- async mkdir(path) {
536
- const headers = {};
537
- if (authHeader) headers["Authorization"] = authHeader;
538
- const res = await fetch(davUrl(path), { method: "MKCOL", headers });
539
- if (!res.ok && res.status !== 405) throw new Error(`WebDAV MKCOL failed: ${res.status}`);
540
- },
541
- async unlink(path) {
542
- await davFetch(path, "DELETE");
543
- },
544
- async rmdir(path) {
545
- const items = await (async () => {
546
- const headers = { Depth: "1" };
547
- if (authHeader) headers["Authorization"] = authHeader;
548
- const res = await fetch(davUrl(path), { method: "PROPFIND", headers });
549
- if (!res.ok) return [];
550
- const parsed = await parseMultiStatus(res);
551
- const prefix = davUrl(path);
552
- return parsed.filter((i) => i.path !== prefix && i.path !== `${prefix}/`);
553
- })();
554
- for (const item of items) {
555
- if (item.isDir) await backend.rmdir(item.path);
556
- else await backend.unlink(item.path);
557
- }
558
- await davFetch(path, "DELETE");
559
- },
560
- async rename(oldPath, newPath) {
561
- const headers = { Destination: davUrl(newPath) };
562
- if (authHeader) headers["Authorization"] = authHeader;
563
- await fetch(davUrl(oldPath), { method: "MOVE", headers });
564
- }
565
- };
566
- return backend;
567
- });
568
- registerBackend("RemoteStorage", async (options) => {
569
- const { RemoteStorageFileSystem } = await import("zen-fs-remotestoragejs");
570
- const href = options.href ?? "";
571
- const token = options.token ?? "";
572
- if (!href) throw new Error('RemoteStorage backend requires "href" option');
573
- if (!token) throw new Error('RemoteStorage backend requires "token" option');
574
- const basePath = options.basePath || void 0;
575
- const fs = new RemoteStorageFileSystem({ href, token, basePath });
576
- return wrapZenFSFileSystem(fs);
577
- });
578
351
 
579
352
  // src/version.ts
580
353
  function versionPathFor(configFilePath) {
@@ -592,9 +365,12 @@ async function sha256(data) {
592
365
  const hex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
593
366
  return `sha256:${hex}`;
594
367
  }
595
- const nodeCrypto = await import("crypto");
596
- const hash = nodeCrypto.createHash("sha256").update(Buffer.from(buffer)).digest("hex");
597
- return `sha256:${hash}`;
368
+ if (typeof globalThis.window === "undefined") {
369
+ const nodeCrypto = await new Function("return import('node:crypto')")();
370
+ const hash = nodeCrypto.createHash("sha256").update(Buffer.from(buffer)).digest("hex");
371
+ return `sha256:${hash}`;
372
+ }
373
+ throw new Error("SHA-256 not available: neither Web Crypto nor Node.js crypto module found");
598
374
  }
599
375
  async function readVersion(fs, versionFilePath) {
600
376
  try {
@@ -1285,7 +1061,9 @@ export {
1285
1061
  readVersion,
1286
1062
  registerBackend,
1287
1063
  sha256,
1064
+ unregisterBackend,
1288
1065
  verifyOrRepairVersion,
1289
1066
  versionPathFor,
1067
+ wrapZenFSFileSystem,
1290
1068
  writeVersion
1291
1069
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.3.17",
3
+ "version": "0.3.19",
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",
@@ -21,6 +21,8 @@
21
21
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
22
  "clean": "rm -rf dist",
23
23
  "typecheck": "tsc --noEmit",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
24
26
  "prepublishOnly": "npm run clean && npm run build"
25
27
  },
26
28
  "keywords": [
@@ -39,22 +41,15 @@
39
41
  "peerDependencies": {
40
42
  "@zenfs/core": ">=2.3.0",
41
43
  "zen-fs-cache": ">=1.0.0",
42
- "zen-fs-gitee": ">=1.0.0",
43
- "zen-fs-github": ">=1.0.0",
44
- "zen-fs-remotestoragejs": ">=1.2.0",
45
44
  "zen-fs-sync": ">=0.1.0"
46
45
  },
47
46
  "devDependencies": {
48
47
  "@zenfs/core": "^2.5.7",
49
48
  "tsup": "^8.5.1",
50
49
  "typescript": "^5.9.3",
50
+ "vitest": "^1.6.1",
51
51
  "zen-fs-cache": "^1.0.1",
52
- "zen-fs-gitee": "^1.0.0",
53
- "zen-fs-github": "^1.0.0",
54
- "zen-fs-remotestoragejs": "^1.2.2",
55
52
  "zen-fs-sync": "^0.1.0"
56
53
  },
57
- "dependencies": {
58
- "@zenfs/dom": "^1.2.9"
59
- }
54
+ "dependencies": {}
60
55
  }