All files / src vault-registry.ts

100% Statements 36/36
100% Branches 18/18
100% Functions 20/20
100% Lines 31/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108                9x 9x     573x 572x       577x 577x               936x       192x       527x       526x       2210x       1237x       286x       286x 286x 286x 286x       3161x 82x 82x   12x                       581x     2101x 2101x       109x       1060x       705x       177x 177x 177x         536x      
/**
 * Shared-root layout + per-vault registry + cross-process lock.
 * One lilbee binary, one HF cache, many vault data-dirs, one active vault at a time.
 */
import { node } from "./node";
import { getDefaultLilbeeDataRoot } from "./session-token";
import { DEFAULT_SHARED_CONFIG, SHARED_PATH, type SharedConfig, type VaultRegistryEntry } from "./types";
 
const VAULT_ID_BYTES = 6;
const FALLBACK_SHARED_ROOT = "/tmp/lilbee";
 
export function resolveSharedRoot(setting: string): string {
    if (setting && setting.length > 0) return setting;
    return getDefaultLilbeeDataRoot() ?? FALLBACK_SHARED_ROOT;
}
 
export function computeVaultId(vaultPath: string): string {
    const canonical = node.resolve(vaultPath);
    return node
        .createHash("sha256")
        .update(canonical)
        .digest("hex")
        .slice(0, VAULT_ID_BYTES * 2);
}
 
export function sharedBinDir(sharedRoot: string): string {
    return node.join(sharedRoot, SHARED_PATH.BIN);
}
 
export function sharedModelsDir(sharedRoot: string): string {
    return node.join(sharedRoot, SHARED_PATH.MODELS);
}
 
export function vaultsRootDir(sharedRoot: string): string {
    return node.join(sharedRoot, SHARED_PATH.VAULTS);
}
 
export function defaultDataDirFor(sharedRoot: string, vaultId: string): string {
    return node.join(vaultsRootDir(sharedRoot), vaultId);
}
 
function configPath(sharedRoot: string): string {
    return node.join(sharedRoot, SHARED_PATH.CONFIG);
}
 
function registryPath(sharedRoot: string): string {
    return node.join(sharedRoot, SHARED_PATH.REGISTRY);
}
 
function ensureDir(path: string): void {
    if (!node.existsSync(path)) node.mkdirSync(path, { recursive: true });
}
 
function writeJsonAtomic(path: string, value: unknown): void {
    ensureDir(node.dirname(path));
    const tmp = `${path}.tmp`;
    node.writeFileSync(tmp, JSON.stringify(value, null, 2));
    node.renameSync(tmp, path);
}
 
function readJson<T>(path: string): T | null {
    if (!node.existsSync(path)) return null;
    try {
        return JSON.parse(node.readFileSync(path, "utf-8")) as T;
    } catch {
        return null;
    }
}
 
/**
 * Manages `<shared-root>/{config.json, registry.json}`. Which server owns the
 * shared root is the server's own business: it holds an OS scope lock and
 * names itself in a sidecar (see server-manager's readScopeOwner).
 * All file I/O is synchronous because the call sites are plugin lifecycle
 * events that already block on disk.
 */
export class VaultRegistry {
    constructor(public readonly sharedRoot: string) {}
 
    loadConfig(): SharedConfig {
        const parsed = readJson<Partial<SharedConfig>>(configPath(this.sharedRoot));
        return { ...DEFAULT_SHARED_CONFIG, ...(parsed ?? {}) };
    }
 
    saveConfig(config: SharedConfig): void {
        writeJsonAtomic(configPath(this.sharedRoot), config);
    }
 
    list(): VaultRegistryEntry[] {
        return readJson<VaultRegistryEntry[]>(registryPath(this.sharedRoot)) ?? [];
    }
 
    get(id: string): VaultRegistryEntry | null {
        return this.list().find((e) => e.id === id) ?? null;
    }
 
    upsert(entry: VaultRegistryEntry): void {
        const entries = this.list().filter((e) => e.id !== entry.id);
        entries.push(entry);
        writeJsonAtomic(registryPath(this.sharedRoot), entries);
    }
 
    /** Return the registered data-dir or the default location for this id. */
    resolveDataDir(id: string): string {
        return this.get(id)?.dataDir ?? defaultDataDirFor(this.sharedRoot, id);
    }
}