@kb-labs/adapters-snapshot-localfs 0.2.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/README.md +10 -0
- package/dist/chunk-OI4P4WIH.js +36 -0
- package/dist/chunk-OI4P4WIH.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +223 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +5 -0
- package/dist/manifest.js +7 -0
- package/dist/manifest.js.map +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @kb-labs/adapters-snapshot-localfs
|
|
2
|
+
|
|
3
|
+
Local filesystem implementation of `ISnapshotProvider`.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Capture snapshot by copying source directory into snapshot store
|
|
8
|
+
- Restore snapshot to target directory
|
|
9
|
+
- Status/delete/garbage-collect lifecycle
|
|
10
|
+
- Can resolve workspace root from workspace registry
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/manifest.ts
|
|
2
|
+
var manifest = {
|
|
3
|
+
manifestVersion: "1.0.0",
|
|
4
|
+
id: "localfs-snapshot-provider",
|
|
5
|
+
name: "LocalFS Snapshot Provider",
|
|
6
|
+
version: "0.1.0",
|
|
7
|
+
description: "Snapshot provider on local filesystem",
|
|
8
|
+
author: "KB Labs Team",
|
|
9
|
+
license: "KBPL-1.1",
|
|
10
|
+
type: "core",
|
|
11
|
+
implements: "ISnapshotProvider",
|
|
12
|
+
contexts: ["workspace"],
|
|
13
|
+
capabilities: {
|
|
14
|
+
custom: {
|
|
15
|
+
provider: "localfs",
|
|
16
|
+
workspaceRegistry: true
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
configSchema: {
|
|
20
|
+
storageDir: {
|
|
21
|
+
type: "string",
|
|
22
|
+
default: ".kb/runtime/snapshots",
|
|
23
|
+
description: "Root directory where snapshots are stored"
|
|
24
|
+
},
|
|
25
|
+
workspaceRegistryDir: {
|
|
26
|
+
type: "string",
|
|
27
|
+
default: ".kb/runtime/workspace-registry",
|
|
28
|
+
description: "Directory with workspace metadata for source path resolution"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
manifest
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=chunk-OI4P4WIH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../Users/kirillbaranov/Desktop/kb-labs-workspace/infra/kb-labs-adapters/packages/adapters-snapshot-localfs/src/manifest.ts"],"sourcesContent":["import type { AdapterManifest } from '@kb-labs/core-platform';\n\nexport const manifest: AdapterManifest = {\n manifestVersion: '1.0.0',\n id: 'localfs-snapshot-provider',\n name: 'LocalFS Snapshot Provider',\n version: '0.1.0',\n description: 'Snapshot provider on local filesystem',\n author: 'KB Labs Team',\n license: 'KBPL-1.1',\n type: 'core',\n implements: 'ISnapshotProvider',\n contexts: ['workspace'],\n capabilities: {\n custom: {\n provider: 'localfs',\n workspaceRegistry: true,\n },\n },\n configSchema: {\n storageDir: {\n type: 'string',\n default: '.kb/runtime/snapshots',\n description: 'Root directory where snapshots are stored',\n },\n workspaceRegistryDir: {\n type: 'string',\n default: '.kb/runtime/workspace-registry',\n description: 'Directory with workspace metadata for source path resolution',\n },\n },\n};\n"],"mappings":";AAEO,IAAM,WAA4B;AAAA,EACvC,iBAAiB;AAAA,EACjB,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,SAAS;AAAA,EACT,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,UAAU,CAAC,WAAW;AAAA,EACtB,cAAc;AAAA,IACZ,QAAQ;AAAA,MACN,UAAU;AAAA,MACV,mBAAmB;AAAA,IACrB;AAAA,EACF;AAAA,EACA,cAAc;AAAA,IACZ,YAAY;AAAA,MACV,MAAM;AAAA,MACN,SAAS;AAAA,MACT,aAAa;AAAA,IACf;AAAA,IACA,sBAAsB;AAAA,MACpB,MAAM;AAAA,MACN,SAAS;AAAA,MACT,aAAa;AAAA,IACf;AAAA,EACF;AACF;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ISnapshotProvider, CaptureSnapshotRequest, SnapshotDescriptor, RestoreSnapshotRequest, RestoreSnapshotResult, SnapshotStatusResult, SnapshotGarbageCollectRequest, SnapshotGarbageCollectResult, SnapshotProviderCapabilities } from '@kb-labs/core-platform';
|
|
2
|
+
export { manifest } from './manifest.js';
|
|
3
|
+
|
|
4
|
+
interface WorkspaceContext {
|
|
5
|
+
cwd?: string;
|
|
6
|
+
}
|
|
7
|
+
interface LocalFsSnapshotAdapterConfig {
|
|
8
|
+
storageDir?: string;
|
|
9
|
+
workspaceRegistryDir?: string;
|
|
10
|
+
workspace?: WorkspaceContext;
|
|
11
|
+
}
|
|
12
|
+
declare class LocalFsSnapshotAdapter implements ISnapshotProvider {
|
|
13
|
+
private readonly config;
|
|
14
|
+
private readonly cwd;
|
|
15
|
+
private readonly storageDir;
|
|
16
|
+
private readonly workspaceRegistryDir;
|
|
17
|
+
private readonly records;
|
|
18
|
+
constructor(config?: LocalFsSnapshotAdapterConfig);
|
|
19
|
+
capture(request: CaptureSnapshotRequest): Promise<SnapshotDescriptor>;
|
|
20
|
+
restore(request: RestoreSnapshotRequest): Promise<RestoreSnapshotResult>;
|
|
21
|
+
getStatus(snapshotId: string): Promise<SnapshotStatusResult>;
|
|
22
|
+
delete(snapshotId: string): Promise<void>;
|
|
23
|
+
garbageCollect(request?: SnapshotGarbageCollectRequest): Promise<SnapshotGarbageCollectResult>;
|
|
24
|
+
getCapabilities(): SnapshotProviderCapabilities;
|
|
25
|
+
private resolveSourcePath;
|
|
26
|
+
private resolveRestoreTargetPath;
|
|
27
|
+
private getRecordPath;
|
|
28
|
+
private persistRecord;
|
|
29
|
+
private getRecordOrThrow;
|
|
30
|
+
private readWorkspaceRecord;
|
|
31
|
+
private loadAllRecords;
|
|
32
|
+
}
|
|
33
|
+
declare function createAdapter(config?: LocalFsSnapshotAdapterConfig): LocalFsSnapshotAdapter;
|
|
34
|
+
|
|
35
|
+
export { LocalFsSnapshotAdapter, type LocalFsSnapshotAdapterConfig, createAdapter, createAdapter as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
manifest
|
|
3
|
+
} from "./chunk-OI4P4WIH.js";
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
import { mkdir, readFile, rm, writeFile, access, cp, readdir, stat } from "fs/promises";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
var LocalFsSnapshotAdapter = class {
|
|
10
|
+
constructor(config = {}) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.cwd = path.resolve(config.workspace?.cwd ?? process.cwd());
|
|
13
|
+
this.storageDir = path.resolve(this.cwd, config.storageDir ?? ".kb/runtime/snapshots");
|
|
14
|
+
this.workspaceRegistryDir = path.resolve(
|
|
15
|
+
this.cwd,
|
|
16
|
+
config.workspaceRegistryDir ?? ".kb/runtime/workspace-registry"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
cwd;
|
|
20
|
+
storageDir;
|
|
21
|
+
workspaceRegistryDir;
|
|
22
|
+
records = /* @__PURE__ */ new Map();
|
|
23
|
+
async capture(request) {
|
|
24
|
+
const snapshotId = request.snapshotId ?? `snap_${randomUUID()}`;
|
|
25
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
26
|
+
const sourcePath = await this.resolveSourcePath(request);
|
|
27
|
+
const namespace = request.namespace ?? "default";
|
|
28
|
+
const payloadPath = path.join(this.storageDir, namespace, snapshotId);
|
|
29
|
+
await mkdir(path.dirname(payloadPath), { recursive: true });
|
|
30
|
+
await rm(payloadPath, { recursive: true, force: true });
|
|
31
|
+
await cp(sourcePath, payloadPath, { recursive: true, force: true });
|
|
32
|
+
const record = {
|
|
33
|
+
snapshotId,
|
|
34
|
+
status: "ready",
|
|
35
|
+
createdAt: now,
|
|
36
|
+
updatedAt: now,
|
|
37
|
+
workspaceId: request.workspaceId,
|
|
38
|
+
environmentId: request.environmentId,
|
|
39
|
+
sourcePath,
|
|
40
|
+
payloadPath,
|
|
41
|
+
namespace
|
|
42
|
+
};
|
|
43
|
+
await this.persistRecord(record);
|
|
44
|
+
return {
|
|
45
|
+
snapshotId,
|
|
46
|
+
provider: "snapshot-localfs",
|
|
47
|
+
status: "ready",
|
|
48
|
+
createdAt: now,
|
|
49
|
+
updatedAt: now,
|
|
50
|
+
workspaceId: request.workspaceId,
|
|
51
|
+
environmentId: request.environmentId,
|
|
52
|
+
metadata: {
|
|
53
|
+
namespace,
|
|
54
|
+
payloadPath
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async restore(request) {
|
|
59
|
+
const record = await this.getRecordOrThrow(request.snapshotId);
|
|
60
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
61
|
+
const targetPath = request.targetPath ? path.resolve(this.cwd, request.targetPath) : await this.resolveRestoreTargetPath(request.workspaceId, record.workspaceId);
|
|
62
|
+
if (!request.overwrite) {
|
|
63
|
+
try {
|
|
64
|
+
await access(targetPath);
|
|
65
|
+
throw new Error(`Restore target already exists: ${targetPath}. Use overwrite=true.`);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error instanceof Error && error.message.startsWith("Restore target already exists")) {
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (request.overwrite) {
|
|
73
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
76
|
+
await cp(record.payloadPath, targetPath, { recursive: true, force: true });
|
|
77
|
+
record.status = "ready";
|
|
78
|
+
record.updatedAt = now;
|
|
79
|
+
await this.persistRecord(record);
|
|
80
|
+
return {
|
|
81
|
+
snapshotId: request.snapshotId,
|
|
82
|
+
restoredAt: now,
|
|
83
|
+
workspaceId: request.workspaceId,
|
|
84
|
+
environmentId: request.environmentId,
|
|
85
|
+
targetPath
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async getStatus(snapshotId) {
|
|
89
|
+
const record = await this.getRecordOrThrow(snapshotId);
|
|
90
|
+
return {
|
|
91
|
+
snapshotId,
|
|
92
|
+
status: record.status,
|
|
93
|
+
updatedAt: record.updatedAt
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async delete(snapshotId) {
|
|
97
|
+
const record = await this.getRecordOrThrow(snapshotId);
|
|
98
|
+
await rm(record.payloadPath, { recursive: true, force: true });
|
|
99
|
+
this.records.delete(snapshotId);
|
|
100
|
+
await rm(this.getRecordPath(snapshotId), { force: true });
|
|
101
|
+
}
|
|
102
|
+
async garbageCollect(request = {}) {
|
|
103
|
+
const beforeTs = request.before ? new Date(request.before).getTime() : Number.POSITIVE_INFINITY;
|
|
104
|
+
const limit = request.limit ?? Number.POSITIVE_INFINITY;
|
|
105
|
+
const dryRun = request.dryRun ?? false;
|
|
106
|
+
let scanned = 0;
|
|
107
|
+
let deleted = 0;
|
|
108
|
+
for (const record of await this.loadAllRecords()) {
|
|
109
|
+
scanned += 1;
|
|
110
|
+
const createdTs = new Date(record.createdAt).getTime();
|
|
111
|
+
if (createdTs >= beforeTs) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (request.namespace && record.namespace !== request.namespace) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (deleted >= limit) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (!dryRun) {
|
|
121
|
+
await this.delete(record.snapshotId);
|
|
122
|
+
}
|
|
123
|
+
deleted += 1;
|
|
124
|
+
}
|
|
125
|
+
return { scanned, deleted, dryRun };
|
|
126
|
+
}
|
|
127
|
+
getCapabilities() {
|
|
128
|
+
return {
|
|
129
|
+
supportsWorkspaceSnapshots: true,
|
|
130
|
+
supportsEnvironmentSnapshots: false,
|
|
131
|
+
supportsGarbageCollection: true,
|
|
132
|
+
supportsIncrementalSnapshots: false,
|
|
133
|
+
custom: {
|
|
134
|
+
provider: "localfs"
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async resolveSourcePath(request) {
|
|
139
|
+
if (request.sourcePath) {
|
|
140
|
+
return path.resolve(this.cwd, request.sourcePath);
|
|
141
|
+
}
|
|
142
|
+
if (request.workspaceId) {
|
|
143
|
+
const workspace = await this.readWorkspaceRecord(request.workspaceId);
|
|
144
|
+
if (workspace.rootPath) {
|
|
145
|
+
return workspace.rootPath;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
throw new Error("Snapshot capture requires sourcePath or resolvable workspaceId");
|
|
149
|
+
}
|
|
150
|
+
async resolveRestoreTargetPath(requestedWorkspaceId, recordWorkspaceId) {
|
|
151
|
+
const workspaceId = requestedWorkspaceId ?? recordWorkspaceId;
|
|
152
|
+
if (!workspaceId) {
|
|
153
|
+
throw new Error("Snapshot restore requires targetPath or workspaceId");
|
|
154
|
+
}
|
|
155
|
+
const workspace = await this.readWorkspaceRecord(workspaceId);
|
|
156
|
+
if (!workspace.rootPath) {
|
|
157
|
+
throw new Error(`Workspace rootPath not found for workspaceId=${workspaceId}`);
|
|
158
|
+
}
|
|
159
|
+
return workspace.rootPath;
|
|
160
|
+
}
|
|
161
|
+
getRecordPath(snapshotId) {
|
|
162
|
+
return path.join(this.storageDir, ".records", `${snapshotId}.json`);
|
|
163
|
+
}
|
|
164
|
+
async persistRecord(record) {
|
|
165
|
+
this.records.set(record.snapshotId, record);
|
|
166
|
+
const recordPath = this.getRecordPath(record.snapshotId);
|
|
167
|
+
await mkdir(path.dirname(recordPath), { recursive: true });
|
|
168
|
+
await writeFile(recordPath, JSON.stringify(record, null, 2), "utf8");
|
|
169
|
+
}
|
|
170
|
+
async getRecordOrThrow(snapshotId) {
|
|
171
|
+
const inMemory = this.records.get(snapshotId);
|
|
172
|
+
if (inMemory) {
|
|
173
|
+
return inMemory;
|
|
174
|
+
}
|
|
175
|
+
const recordPath = this.getRecordPath(snapshotId);
|
|
176
|
+
try {
|
|
177
|
+
const raw = await readFile(recordPath, "utf8");
|
|
178
|
+
const parsed = JSON.parse(raw);
|
|
179
|
+
this.records.set(snapshotId, parsed);
|
|
180
|
+
return parsed;
|
|
181
|
+
} catch {
|
|
182
|
+
throw new Error(`Snapshot not found: ${snapshotId}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async readWorkspaceRecord(workspaceId) {
|
|
186
|
+
const filePath = path.join(this.workspaceRegistryDir, `${workspaceId}.json`);
|
|
187
|
+
const raw = await readFile(filePath, "utf8");
|
|
188
|
+
return JSON.parse(raw);
|
|
189
|
+
}
|
|
190
|
+
async loadAllRecords() {
|
|
191
|
+
const recordsDir = path.join(this.storageDir, ".records");
|
|
192
|
+
try {
|
|
193
|
+
const entries = await readdir(recordsDir);
|
|
194
|
+
const loaded = [];
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
if (!entry.endsWith(".json")) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const filePath = path.join(recordsDir, entry);
|
|
200
|
+
const fileStat = await stat(filePath);
|
|
201
|
+
if (!fileStat.isFile()) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const raw = await readFile(filePath, "utf8");
|
|
205
|
+
loaded.push(JSON.parse(raw));
|
|
206
|
+
}
|
|
207
|
+
return loaded;
|
|
208
|
+
} catch {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
function createAdapter(config) {
|
|
214
|
+
return new LocalFsSnapshotAdapter(config);
|
|
215
|
+
}
|
|
216
|
+
var index_default = createAdapter;
|
|
217
|
+
export {
|
|
218
|
+
LocalFsSnapshotAdapter,
|
|
219
|
+
createAdapter,
|
|
220
|
+
index_default as default,
|
|
221
|
+
manifest
|
|
222
|
+
};
|
|
223
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../Users/kirillbaranov/Desktop/kb-labs-workspace/infra/kb-labs-adapters/packages/adapters-snapshot-localfs/src/index.ts"],"sourcesContent":["import { mkdir, readFile, rm, writeFile, access, cp, readdir, stat } from 'node:fs/promises';\nimport path from 'node:path';\nimport { randomUUID } from 'node:crypto';\nimport type {\n ISnapshotProvider,\n CaptureSnapshotRequest,\n SnapshotDescriptor,\n RestoreSnapshotRequest,\n RestoreSnapshotResult,\n SnapshotStatusResult,\n SnapshotGarbageCollectRequest,\n SnapshotGarbageCollectResult,\n SnapshotProviderCapabilities,\n SnapshotStatus,\n} from '@kb-labs/core-platform';\n\nexport { manifest } from './manifest.js';\n\ninterface WorkspaceContext {\n cwd?: string;\n}\n\nexport interface LocalFsSnapshotAdapterConfig {\n storageDir?: string;\n workspaceRegistryDir?: string;\n workspace?: WorkspaceContext;\n}\n\ninterface SnapshotRecord {\n snapshotId: string;\n status: SnapshotStatus;\n createdAt: string;\n updatedAt: string;\n workspaceId?: string;\n environmentId?: string;\n sourcePath: string;\n payloadPath: string;\n namespace?: string;\n}\n\ninterface WorkspaceRecord {\n workspaceId: string;\n rootPath?: string;\n}\n\nexport class LocalFsSnapshotAdapter implements ISnapshotProvider {\n private readonly cwd: string;\n private readonly storageDir: string;\n private readonly workspaceRegistryDir: string;\n private readonly records = new Map<string, SnapshotRecord>();\n\n constructor(private readonly config: LocalFsSnapshotAdapterConfig = {}) {\n this.cwd = path.resolve(config.workspace?.cwd ?? process.cwd());\n this.storageDir = path.resolve(this.cwd, config.storageDir ?? '.kb/runtime/snapshots');\n this.workspaceRegistryDir = path.resolve(\n this.cwd,\n config.workspaceRegistryDir ?? '.kb/runtime/workspace-registry'\n );\n }\n\n async capture(request: CaptureSnapshotRequest): Promise<SnapshotDescriptor> {\n const snapshotId = request.snapshotId ?? `snap_${randomUUID()}`;\n const now = new Date().toISOString();\n const sourcePath = await this.resolveSourcePath(request);\n const namespace = request.namespace ?? 'default';\n const payloadPath = path.join(this.storageDir, namespace, snapshotId);\n\n await mkdir(path.dirname(payloadPath), { recursive: true });\n await rm(payloadPath, { recursive: true, force: true });\n await cp(sourcePath, payloadPath, { recursive: true, force: true });\n\n const record: SnapshotRecord = {\n snapshotId,\n status: 'ready',\n createdAt: now,\n updatedAt: now,\n workspaceId: request.workspaceId,\n environmentId: request.environmentId,\n sourcePath,\n payloadPath,\n namespace,\n };\n\n await this.persistRecord(record);\n\n return {\n snapshotId,\n provider: 'snapshot-localfs',\n status: 'ready',\n createdAt: now,\n updatedAt: now,\n workspaceId: request.workspaceId,\n environmentId: request.environmentId,\n metadata: {\n namespace,\n payloadPath,\n },\n };\n }\n\n async restore(request: RestoreSnapshotRequest): Promise<RestoreSnapshotResult> {\n const record = await this.getRecordOrThrow(request.snapshotId);\n const now = new Date().toISOString();\n\n const targetPath = request.targetPath\n ? path.resolve(this.cwd, request.targetPath)\n : await this.resolveRestoreTargetPath(request.workspaceId, record.workspaceId);\n\n if (!request.overwrite) {\n try {\n await access(targetPath);\n throw new Error(`Restore target already exists: ${targetPath}. Use overwrite=true.`);\n } catch (error) {\n if (error instanceof Error && error.message.startsWith('Restore target already exists')) {\n throw error;\n }\n // path missing, continue\n }\n }\n\n if (request.overwrite) {\n await rm(targetPath, { recursive: true, force: true });\n }\n\n await mkdir(path.dirname(targetPath), { recursive: true });\n await cp(record.payloadPath, targetPath, { recursive: true, force: true });\n\n record.status = 'ready';\n record.updatedAt = now;\n await this.persistRecord(record);\n\n return {\n snapshotId: request.snapshotId,\n restoredAt: now,\n workspaceId: request.workspaceId,\n environmentId: request.environmentId,\n targetPath,\n };\n }\n\n async getStatus(snapshotId: string): Promise<SnapshotStatusResult> {\n const record = await this.getRecordOrThrow(snapshotId);\n return {\n snapshotId,\n status: record.status,\n updatedAt: record.updatedAt,\n };\n }\n\n async delete(snapshotId: string): Promise<void> {\n const record = await this.getRecordOrThrow(snapshotId);\n await rm(record.payloadPath, { recursive: true, force: true });\n this.records.delete(snapshotId);\n await rm(this.getRecordPath(snapshotId), { force: true });\n }\n\n async garbageCollect(\n request: SnapshotGarbageCollectRequest = {}\n ): Promise<SnapshotGarbageCollectResult> {\n const beforeTs = request.before ? new Date(request.before).getTime() : Number.POSITIVE_INFINITY;\n const limit = request.limit ?? Number.POSITIVE_INFINITY;\n const dryRun = request.dryRun ?? false;\n let scanned = 0;\n let deleted = 0;\n\n for (const record of await this.loadAllRecords()) {\n scanned += 1;\n const createdTs = new Date(record.createdAt).getTime();\n if (createdTs >= beforeTs) {\n continue;\n }\n if (request.namespace && record.namespace !== request.namespace) {\n continue;\n }\n if (deleted >= limit) {\n break;\n }\n\n if (!dryRun) {\n await this.delete(record.snapshotId);\n }\n deleted += 1;\n }\n\n return { scanned, deleted, dryRun };\n }\n\n getCapabilities(): SnapshotProviderCapabilities {\n return {\n supportsWorkspaceSnapshots: true,\n supportsEnvironmentSnapshots: false,\n supportsGarbageCollection: true,\n supportsIncrementalSnapshots: false,\n custom: {\n provider: 'localfs',\n },\n };\n }\n\n private async resolveSourcePath(request: CaptureSnapshotRequest): Promise<string> {\n if (request.sourcePath) {\n return path.resolve(this.cwd, request.sourcePath);\n }\n\n if (request.workspaceId) {\n const workspace = await this.readWorkspaceRecord(request.workspaceId);\n if (workspace.rootPath) {\n return workspace.rootPath;\n }\n }\n\n throw new Error('Snapshot capture requires sourcePath or resolvable workspaceId');\n }\n\n private async resolveRestoreTargetPath(\n requestedWorkspaceId?: string,\n recordWorkspaceId?: string\n ): Promise<string> {\n const workspaceId = requestedWorkspaceId ?? recordWorkspaceId;\n if (!workspaceId) {\n throw new Error('Snapshot restore requires targetPath or workspaceId');\n }\n const workspace = await this.readWorkspaceRecord(workspaceId);\n if (!workspace.rootPath) {\n throw new Error(`Workspace rootPath not found for workspaceId=${workspaceId}`);\n }\n return workspace.rootPath;\n }\n\n private getRecordPath(snapshotId: string): string {\n return path.join(this.storageDir, '.records', `${snapshotId}.json`);\n }\n\n private async persistRecord(record: SnapshotRecord): Promise<void> {\n this.records.set(record.snapshotId, record);\n const recordPath = this.getRecordPath(record.snapshotId);\n await mkdir(path.dirname(recordPath), { recursive: true });\n await writeFile(recordPath, JSON.stringify(record, null, 2), 'utf8');\n }\n\n private async getRecordOrThrow(snapshotId: string): Promise<SnapshotRecord> {\n const inMemory = this.records.get(snapshotId);\n if (inMemory) {\n return inMemory;\n }\n\n const recordPath = this.getRecordPath(snapshotId);\n try {\n const raw = await readFile(recordPath, 'utf8');\n const parsed = JSON.parse(raw) as SnapshotRecord;\n this.records.set(snapshotId, parsed);\n return parsed;\n } catch {\n throw new Error(`Snapshot not found: ${snapshotId}`);\n }\n }\n\n private async readWorkspaceRecord(workspaceId: string): Promise<WorkspaceRecord> {\n const filePath = path.join(this.workspaceRegistryDir, `${workspaceId}.json`);\n const raw = await readFile(filePath, 'utf8');\n return JSON.parse(raw) as WorkspaceRecord;\n }\n\n private async loadAllRecords(): Promise<SnapshotRecord[]> {\n const recordsDir = path.join(this.storageDir, '.records');\n try {\n const entries = await readdir(recordsDir);\n const loaded: SnapshotRecord[] = [];\n for (const entry of entries) {\n if (!entry.endsWith('.json')) {\n continue;\n }\n const filePath = path.join(recordsDir, entry);\n const fileStat = await stat(filePath);\n if (!fileStat.isFile()) {\n continue;\n }\n const raw = await readFile(filePath, 'utf8');\n loaded.push(JSON.parse(raw) as SnapshotRecord);\n }\n return loaded;\n } catch {\n return [];\n }\n }\n}\n\nexport function createAdapter(config?: LocalFsSnapshotAdapterConfig): LocalFsSnapshotAdapter {\n return new LocalFsSnapshotAdapter(config);\n}\n\nexport default createAdapter;\n"],"mappings":"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","names":[]}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kb-labs/adapters-snapshot-localfs",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Local filesystem adapter implementing ISnapshotProvider",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"scripts": {
|
|
20
|
+
"clean": "rimraf dist",
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"dev": "tsup --watch",
|
|
23
|
+
"type-check": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run --passWithNoTests",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"lint": "eslint src --ext .ts",
|
|
27
|
+
"lint:fix": "eslint . --fix"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@kb-labs/core-platform": "*"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@kb-labs/core-platform": "link:../../../../platform/kb-labs-core/packages/core-platform",
|
|
34
|
+
"@types/node": "^24.3.3",
|
|
35
|
+
"eslint": "^9",
|
|
36
|
+
"tsup": "^8.5.0",
|
|
37
|
+
"typescript": "^5.6.3",
|
|
38
|
+
"vitest": "^3.2.4",
|
|
39
|
+
"@kb-labs/devkit": "link:../../../kb-labs-devkit",
|
|
40
|
+
"rimraf": "^6.0.1"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20.0.0",
|
|
44
|
+
"pnpm": ">=9.0.0"
|
|
45
|
+
}
|
|
46
|
+
}
|