@ema.co/mcp-toolkit 0.2.3 → 0.3.0
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/mcp/handlers-consolidated.js +248 -1
- package/dist/mcp/server.js +44 -3
- package/dist/mcp/tools-consolidated.js +19 -3
- package/dist/sdk/index.js +8 -0
- package/dist/sdk/version-policy.js +328 -0
- package/dist/sdk/version-storage.js +465 -0
- package/dist/sdk/version-tracking.js +346 -0
- package/package.json +1 -1
- package/docs/advisor-comms-assistant-fixes.md +0 -175
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persona Version Storage - Git-backed Local Storage
|
|
3
|
+
*
|
|
4
|
+
* Provides file-based storage for version snapshots with content-addressed
|
|
5
|
+
* blob storage for deduplication.
|
|
6
|
+
*
|
|
7
|
+
* Directory structure:
|
|
8
|
+
* .ema-versions/
|
|
9
|
+
* ├── config.yaml # Global version tracking config
|
|
10
|
+
* ├── policies/
|
|
11
|
+
* │ └── {persona_id}.yaml # Per-persona policies
|
|
12
|
+
* ├── snapshots/
|
|
13
|
+
* │ └── {persona_id}/
|
|
14
|
+
* │ ├── manifest.json # Index of all versions
|
|
15
|
+
* │ ├── v001.json # Full snapshot
|
|
16
|
+
* │ └── latest.json # Copy of latest (for quick access)
|
|
17
|
+
* └── blobs/ # Content-addressed storage
|
|
18
|
+
* └── {content_hash}.json # Deduplicated snapshot content
|
|
19
|
+
*/
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { nowIso } from "./version-tracking.js";
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
// Constants
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
const DEFAULT_BASE_DIR = ".ema-versions";
|
|
27
|
+
const SNAPSHOTS_DIR = "snapshots";
|
|
28
|
+
const BLOBS_DIR = "blobs";
|
|
29
|
+
const POLICIES_DIR = "policies";
|
|
30
|
+
const CONFIG_FILE = "config.yaml";
|
|
31
|
+
const MANIFEST_FILE = "manifest.json";
|
|
32
|
+
const LATEST_FILE = "latest.json";
|
|
33
|
+
const DEFAULT_CONFIG = {
|
|
34
|
+
default_auto_version_on_deploy: true,
|
|
35
|
+
default_auto_version_on_sync: true,
|
|
36
|
+
default_max_versions: 50,
|
|
37
|
+
default_prune_strategy: "oldest",
|
|
38
|
+
enable_blob_dedup: true,
|
|
39
|
+
};
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// Version Storage Class
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
export class VersionStorage {
|
|
44
|
+
baseDir;
|
|
45
|
+
config;
|
|
46
|
+
constructor(workspaceRoot, baseDir) {
|
|
47
|
+
this.baseDir = path.join(workspaceRoot, baseDir ?? DEFAULT_BASE_DIR);
|
|
48
|
+
this.config = DEFAULT_CONFIG;
|
|
49
|
+
this.ensureDirectories();
|
|
50
|
+
this.loadConfig();
|
|
51
|
+
}
|
|
52
|
+
// ─────────────── Directory Management ───────────────
|
|
53
|
+
ensureDirectories() {
|
|
54
|
+
const dirs = [
|
|
55
|
+
this.baseDir,
|
|
56
|
+
path.join(this.baseDir, SNAPSHOTS_DIR),
|
|
57
|
+
path.join(this.baseDir, BLOBS_DIR),
|
|
58
|
+
path.join(this.baseDir, POLICIES_DIR),
|
|
59
|
+
];
|
|
60
|
+
for (const dir of dirs) {
|
|
61
|
+
if (!fs.existsSync(dir)) {
|
|
62
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
getSnapshotDir(personaId) {
|
|
67
|
+
return path.join(this.baseDir, SNAPSHOTS_DIR, personaId);
|
|
68
|
+
}
|
|
69
|
+
ensurePersonaDir(personaId) {
|
|
70
|
+
const dir = this.getSnapshotDir(personaId);
|
|
71
|
+
if (!fs.existsSync(dir)) {
|
|
72
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// ─────────────── Config Management ───────────────
|
|
76
|
+
loadConfig() {
|
|
77
|
+
const configPath = path.join(this.baseDir, CONFIG_FILE);
|
|
78
|
+
if (fs.existsSync(configPath)) {
|
|
79
|
+
try {
|
|
80
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
81
|
+
// Simple YAML parsing for flat config (avoids dependency)
|
|
82
|
+
const parsed = this.parseSimpleYaml(content);
|
|
83
|
+
this.config = { ...DEFAULT_CONFIG, ...parsed };
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Use defaults on error
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
parseSimpleYaml(content) {
|
|
91
|
+
const result = {};
|
|
92
|
+
const lines = content.split("\n");
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
const trimmed = line.trim();
|
|
95
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
96
|
+
continue;
|
|
97
|
+
const colonIdx = trimmed.indexOf(":");
|
|
98
|
+
if (colonIdx === -1)
|
|
99
|
+
continue;
|
|
100
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
101
|
+
let value = trimmed.slice(colonIdx + 1).trim();
|
|
102
|
+
// Type coercion
|
|
103
|
+
if (value === "true")
|
|
104
|
+
value = true;
|
|
105
|
+
else if (value === "false")
|
|
106
|
+
value = false;
|
|
107
|
+
else if (/^\d+$/.test(value))
|
|
108
|
+
value = parseInt(value, 10);
|
|
109
|
+
result[key] = value;
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
saveConfig() {
|
|
114
|
+
const configPath = path.join(this.baseDir, CONFIG_FILE);
|
|
115
|
+
const lines = [
|
|
116
|
+
"# Ema Version Tracking Configuration",
|
|
117
|
+
`default_auto_version_on_deploy: ${this.config.default_auto_version_on_deploy}`,
|
|
118
|
+
`default_auto_version_on_sync: ${this.config.default_auto_version_on_sync}`,
|
|
119
|
+
`default_max_versions: ${this.config.default_max_versions}`,
|
|
120
|
+
`default_prune_strategy: ${this.config.default_prune_strategy}`,
|
|
121
|
+
`enable_blob_dedup: ${this.config.enable_blob_dedup}`,
|
|
122
|
+
];
|
|
123
|
+
fs.writeFileSync(configPath, lines.join("\n") + "\n");
|
|
124
|
+
}
|
|
125
|
+
getConfig() {
|
|
126
|
+
return { ...this.config };
|
|
127
|
+
}
|
|
128
|
+
updateConfig(updates) {
|
|
129
|
+
this.config = { ...this.config, ...updates };
|
|
130
|
+
this.saveConfig();
|
|
131
|
+
}
|
|
132
|
+
// ─────────────── Manifest Management ───────────────
|
|
133
|
+
getManifestPath(personaId) {
|
|
134
|
+
return path.join(this.getSnapshotDir(personaId), MANIFEST_FILE);
|
|
135
|
+
}
|
|
136
|
+
getManifest(personaId) {
|
|
137
|
+
const manifestPath = this.getManifestPath(personaId);
|
|
138
|
+
if (!fs.existsSync(manifestPath)) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const content = fs.readFileSync(manifestPath, "utf8");
|
|
143
|
+
return JSON.parse(content);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
saveManifest(personaId, manifest) {
|
|
150
|
+
this.ensurePersonaDir(personaId);
|
|
151
|
+
const manifestPath = this.getManifestPath(personaId);
|
|
152
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
153
|
+
}
|
|
154
|
+
createEmptyManifest(personaId, environment) {
|
|
155
|
+
return {
|
|
156
|
+
persona_id: personaId,
|
|
157
|
+
environment,
|
|
158
|
+
latest_version_id: null,
|
|
159
|
+
latest_version_number: 0,
|
|
160
|
+
versions: [],
|
|
161
|
+
updated_at: nowIso(),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// ─────────────── Blob Storage ───────────────
|
|
165
|
+
getBlobPath(contentHash) {
|
|
166
|
+
return path.join(this.baseDir, BLOBS_DIR, `${contentHash}.json`);
|
|
167
|
+
}
|
|
168
|
+
saveBlob(contentHash, snapshot) {
|
|
169
|
+
const blobPath = this.getBlobPath(contentHash);
|
|
170
|
+
if (!fs.existsSync(blobPath)) {
|
|
171
|
+
fs.writeFileSync(blobPath, JSON.stringify(snapshot, null, 2));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
loadBlob(contentHash) {
|
|
175
|
+
const blobPath = this.getBlobPath(contentHash);
|
|
176
|
+
if (!fs.existsSync(blobPath)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const content = fs.readFileSync(blobPath, "utf8");
|
|
181
|
+
return JSON.parse(content);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// ─────────────── Version Snapshot Storage ───────────────
|
|
188
|
+
getVersionPath(personaId, versionNumber) {
|
|
189
|
+
const paddedNum = String(versionNumber).padStart(3, "0");
|
|
190
|
+
return path.join(this.getSnapshotDir(personaId), `v${paddedNum}.json`);
|
|
191
|
+
}
|
|
192
|
+
getLatestPath(personaId) {
|
|
193
|
+
return path.join(this.getSnapshotDir(personaId), LATEST_FILE);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Save a version snapshot to storage.
|
|
197
|
+
*/
|
|
198
|
+
saveVersion(version) {
|
|
199
|
+
this.ensurePersonaDir(version.persona_id);
|
|
200
|
+
// Save blob if dedup enabled
|
|
201
|
+
if (this.config.enable_blob_dedup) {
|
|
202
|
+
this.saveBlob(version.content_hash, version.snapshot);
|
|
203
|
+
}
|
|
204
|
+
// Save full version snapshot
|
|
205
|
+
const versionPath = this.getVersionPath(version.persona_id, version.version_number);
|
|
206
|
+
fs.writeFileSync(versionPath, JSON.stringify(version, null, 2));
|
|
207
|
+
// Update latest
|
|
208
|
+
const latestPath = this.getLatestPath(version.persona_id);
|
|
209
|
+
fs.writeFileSync(latestPath, JSON.stringify(version, null, 2));
|
|
210
|
+
// Update manifest
|
|
211
|
+
let manifest = this.getManifest(version.persona_id);
|
|
212
|
+
if (!manifest) {
|
|
213
|
+
manifest = this.createEmptyManifest(version.persona_id, version.environment);
|
|
214
|
+
}
|
|
215
|
+
const entry = {
|
|
216
|
+
id: version.id,
|
|
217
|
+
version_number: version.version_number,
|
|
218
|
+
version_name: version.version_name,
|
|
219
|
+
content_hash: version.content_hash,
|
|
220
|
+
created_at: version.created_at,
|
|
221
|
+
created_by: version.created_by,
|
|
222
|
+
trigger: version.trigger,
|
|
223
|
+
message: version.message,
|
|
224
|
+
parent_version_id: version.parent_version_id,
|
|
225
|
+
};
|
|
226
|
+
manifest.versions.push(entry);
|
|
227
|
+
manifest.latest_version_id = version.id;
|
|
228
|
+
manifest.latest_version_number = version.version_number;
|
|
229
|
+
manifest.updated_at = nowIso();
|
|
230
|
+
this.saveManifest(version.persona_id, manifest);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Get a specific version by number.
|
|
234
|
+
*/
|
|
235
|
+
getVersion(personaId, versionNumber) {
|
|
236
|
+
const versionPath = this.getVersionPath(personaId, versionNumber);
|
|
237
|
+
if (!fs.existsSync(versionPath)) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const content = fs.readFileSync(versionPath, "utf8");
|
|
242
|
+
return JSON.parse(content);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Get a version by ID.
|
|
250
|
+
*/
|
|
251
|
+
getVersionById(personaId, versionId) {
|
|
252
|
+
const manifest = this.getManifest(personaId);
|
|
253
|
+
if (!manifest)
|
|
254
|
+
return null;
|
|
255
|
+
const entry = manifest.versions.find((v) => v.id === versionId);
|
|
256
|
+
if (!entry)
|
|
257
|
+
return null;
|
|
258
|
+
return this.getVersion(personaId, entry.version_number);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Get a version by name (e.g., "v3", "latest").
|
|
262
|
+
*/
|
|
263
|
+
getVersionByName(personaId, versionName) {
|
|
264
|
+
if (versionName === "latest") {
|
|
265
|
+
return this.getLatestVersion(personaId);
|
|
266
|
+
}
|
|
267
|
+
const manifest = this.getManifest(personaId);
|
|
268
|
+
if (!manifest)
|
|
269
|
+
return null;
|
|
270
|
+
const entry = manifest.versions.find((v) => v.version_name === versionName);
|
|
271
|
+
if (!entry)
|
|
272
|
+
return null;
|
|
273
|
+
return this.getVersion(personaId, entry.version_number);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Get the latest version.
|
|
277
|
+
*/
|
|
278
|
+
getLatestVersion(personaId) {
|
|
279
|
+
const latestPath = this.getLatestPath(personaId);
|
|
280
|
+
if (!fs.existsSync(latestPath)) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const content = fs.readFileSync(latestPath, "utf8");
|
|
285
|
+
return JSON.parse(content);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* List all versions for a persona (from manifest).
|
|
293
|
+
*/
|
|
294
|
+
listVersions(personaId) {
|
|
295
|
+
const manifest = this.getManifest(personaId);
|
|
296
|
+
return manifest?.versions ?? [];
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Check if a content hash already exists (for deduplication).
|
|
300
|
+
*/
|
|
301
|
+
hasContentHash(personaId, contentHash) {
|
|
302
|
+
const manifest = this.getManifest(personaId);
|
|
303
|
+
if (!manifest)
|
|
304
|
+
return false;
|
|
305
|
+
return manifest.versions.some((v) => v.content_hash === contentHash);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Get the next version number for a persona.
|
|
309
|
+
*/
|
|
310
|
+
getNextVersionNumber(personaId) {
|
|
311
|
+
const manifest = this.getManifest(personaId);
|
|
312
|
+
return (manifest?.latest_version_number ?? 0) + 1;
|
|
313
|
+
}
|
|
314
|
+
// ─────────────── Policy Management ───────────────
|
|
315
|
+
getPolicyPath(personaId) {
|
|
316
|
+
return path.join(this.baseDir, POLICIES_DIR, `${personaId}.yaml`);
|
|
317
|
+
}
|
|
318
|
+
getPolicy(personaId) {
|
|
319
|
+
const policyPath = this.getPolicyPath(personaId);
|
|
320
|
+
if (fs.existsSync(policyPath)) {
|
|
321
|
+
try {
|
|
322
|
+
const content = fs.readFileSync(policyPath, "utf8");
|
|
323
|
+
const parsed = this.parseSimpleYaml(content);
|
|
324
|
+
return {
|
|
325
|
+
persona_id: personaId,
|
|
326
|
+
auto_version_on_deploy: parsed.auto_version_on_deploy ?? this.config.default_auto_version_on_deploy,
|
|
327
|
+
auto_version_on_sync: parsed.auto_version_on_sync ?? this.config.default_auto_version_on_sync,
|
|
328
|
+
max_versions: parsed.max_versions ?? this.config.default_max_versions,
|
|
329
|
+
prune_strategy: parsed.prune_strategy ?? this.config.default_prune_strategy,
|
|
330
|
+
min_interval: parsed.min_interval,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// Return defaults on error
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Return default policy
|
|
338
|
+
return {
|
|
339
|
+
persona_id: personaId,
|
|
340
|
+
auto_version_on_deploy: this.config.default_auto_version_on_deploy,
|
|
341
|
+
auto_version_on_sync: this.config.default_auto_version_on_sync,
|
|
342
|
+
max_versions: this.config.default_max_versions,
|
|
343
|
+
prune_strategy: this.config.default_prune_strategy,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
savePolicy(policy) {
|
|
347
|
+
const policyPath = this.getPolicyPath(policy.persona_id);
|
|
348
|
+
const lines = [
|
|
349
|
+
`# Version policy for persona ${policy.persona_id}`,
|
|
350
|
+
`auto_version_on_deploy: ${policy.auto_version_on_deploy}`,
|
|
351
|
+
`auto_version_on_sync: ${policy.auto_version_on_sync}`,
|
|
352
|
+
`max_versions: ${policy.max_versions}`,
|
|
353
|
+
`prune_strategy: ${policy.prune_strategy}`,
|
|
354
|
+
];
|
|
355
|
+
if (policy.min_interval) {
|
|
356
|
+
lines.push(`min_interval: ${policy.min_interval}`);
|
|
357
|
+
}
|
|
358
|
+
fs.writeFileSync(policyPath, lines.join("\n") + "\n");
|
|
359
|
+
}
|
|
360
|
+
// ─────────────── Pruning ───────────────
|
|
361
|
+
/**
|
|
362
|
+
* Prune old versions according to policy.
|
|
363
|
+
* Returns the number of versions pruned.
|
|
364
|
+
*/
|
|
365
|
+
pruneVersions(personaId) {
|
|
366
|
+
const policy = this.getPolicy(personaId);
|
|
367
|
+
if (policy.max_versions === 0) {
|
|
368
|
+
return 0; // Unlimited
|
|
369
|
+
}
|
|
370
|
+
const manifest = this.getManifest(personaId);
|
|
371
|
+
if (!manifest || manifest.versions.length <= policy.max_versions) {
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
// Sort by version number (ascending)
|
|
375
|
+
const sorted = [...manifest.versions].sort((a, b) => a.version_number - b.version_number);
|
|
376
|
+
// Determine which to keep
|
|
377
|
+
let toKeep;
|
|
378
|
+
if (policy.prune_strategy === "keep_tagged") {
|
|
379
|
+
// Keep tagged versions (those with custom names or non-empty messages)
|
|
380
|
+
const tagged = sorted.filter((v) => !v.version_name.match(/^v\d+$/) || v.message.trim().length > 0);
|
|
381
|
+
const untagged = sorted.filter((v) => v.version_name.match(/^v\d+$/) && v.message.trim().length === 0);
|
|
382
|
+
// Keep all tagged + most recent untagged up to limit
|
|
383
|
+
const keepCount = Math.max(0, policy.max_versions - tagged.length);
|
|
384
|
+
toKeep = [...tagged, ...untagged.slice(-keepCount)];
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
// Keep newest
|
|
388
|
+
toKeep = sorted.slice(-policy.max_versions);
|
|
389
|
+
}
|
|
390
|
+
const keepIds = new Set(toKeep.map((v) => v.id));
|
|
391
|
+
const toDelete = sorted.filter((v) => !keepIds.has(v.id));
|
|
392
|
+
// Delete version files
|
|
393
|
+
for (const v of toDelete) {
|
|
394
|
+
const versionPath = this.getVersionPath(personaId, v.version_number);
|
|
395
|
+
if (fs.existsSync(versionPath)) {
|
|
396
|
+
fs.unlinkSync(versionPath);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Update manifest
|
|
400
|
+
manifest.versions = toKeep.sort((a, b) => a.version_number - b.version_number);
|
|
401
|
+
manifest.updated_at = nowIso();
|
|
402
|
+
this.saveManifest(personaId, manifest);
|
|
403
|
+
return toDelete.length;
|
|
404
|
+
}
|
|
405
|
+
// ─────────────── Utilities ───────────────
|
|
406
|
+
/**
|
|
407
|
+
* List all personas that have version history.
|
|
408
|
+
*/
|
|
409
|
+
listPersonasWithVersions() {
|
|
410
|
+
const snapshotsDir = path.join(this.baseDir, SNAPSHOTS_DIR);
|
|
411
|
+
if (!fs.existsSync(snapshotsDir)) {
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
return fs
|
|
415
|
+
.readdirSync(snapshotsDir, { withFileTypes: true })
|
|
416
|
+
.filter((d) => d.isDirectory())
|
|
417
|
+
.map((d) => d.name);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Get storage statistics.
|
|
421
|
+
*/
|
|
422
|
+
getStats() {
|
|
423
|
+
const personas = this.listPersonasWithVersions();
|
|
424
|
+
let totalVersions = 0;
|
|
425
|
+
let storageBytes = 0;
|
|
426
|
+
for (const personaId of personas) {
|
|
427
|
+
const manifest = this.getManifest(personaId);
|
|
428
|
+
if (manifest) {
|
|
429
|
+
totalVersions += manifest.versions.length;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// Count blobs
|
|
433
|
+
const blobsDir = path.join(this.baseDir, BLOBS_DIR);
|
|
434
|
+
let totalBlobs = 0;
|
|
435
|
+
if (fs.existsSync(blobsDir)) {
|
|
436
|
+
const blobs = fs.readdirSync(blobsDir);
|
|
437
|
+
totalBlobs = blobs.length;
|
|
438
|
+
for (const blob of blobs) {
|
|
439
|
+
const stat = fs.statSync(path.join(blobsDir, blob));
|
|
440
|
+
storageBytes += stat.size;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
total_personas: personas.length,
|
|
445
|
+
total_versions: totalVersions,
|
|
446
|
+
total_blobs: totalBlobs,
|
|
447
|
+
storage_bytes: storageBytes,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Get the base directory path.
|
|
452
|
+
*/
|
|
453
|
+
getBaseDir() {
|
|
454
|
+
return this.baseDir;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
458
|
+
// Factory Function
|
|
459
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
460
|
+
/**
|
|
461
|
+
* Create a VersionStorage instance.
|
|
462
|
+
*/
|
|
463
|
+
export function createVersionStorage(workspaceRoot, baseDir) {
|
|
464
|
+
return new VersionStorage(workspaceRoot, baseDir);
|
|
465
|
+
}
|