@caryhu/codemine-forge 0.0.1-alpha.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 +108 -0
- package/dist/build-pipeline/browser-bundle.d.ts +31 -0
- package/dist/build-pipeline/browser-bundle.js +584 -0
- package/dist/build-pipeline/diagnostics.d.ts +7 -0
- package/dist/build-pipeline/diagnostics.js +68 -0
- package/dist/build-pipeline/index.d.ts +3 -0
- package/dist/build-pipeline/index.js +16 -0
- package/dist/build-pipeline/pipeline.d.ts +12 -0
- package/dist/build-pipeline/pipeline.js +273 -0
- package/dist/build-pipeline/types.d.ts +77 -0
- package/dist/build-pipeline/types.js +4 -0
- package/dist/dev-server-preview/diagnostics.d.ts +5 -0
- package/dist/dev-server-preview/diagnostics.js +40 -0
- package/dist/dev-server-preview/index.d.ts +4 -0
- package/dist/dev-server-preview/index.js +16 -0
- package/dist/dev-server-preview/preview-document.d.ts +2 -0
- package/dist/dev-server-preview/preview-document.js +153 -0
- package/dist/dev-server-preview/server.d.ts +26 -0
- package/dist/dev-server-preview/server.js +279 -0
- package/dist/dev-server-preview/types.d.ts +105 -0
- package/dist/dev-server-preview/types.js +4 -0
- package/dist/esbuild-wasm-worker/diagnostics.d.ts +12 -0
- package/dist/esbuild-wasm-worker/diagnostics.js +53 -0
- package/dist/esbuild-wasm-worker/index.d.ts +4 -0
- package/dist/esbuild-wasm-worker/index.js +18 -0
- package/dist/esbuild-wasm-worker/service.d.ts +26 -0
- package/dist/esbuild-wasm-worker/service.js +260 -0
- package/dist/esbuild-wasm-worker/types.d.ts +139 -0
- package/dist/esbuild-wasm-worker/types.js +5 -0
- package/dist/esbuild-wasm-worker/worker-adapter.d.ts +38 -0
- package/dist/esbuild-wasm-worker/worker-adapter.js +165 -0
- package/dist/esbuild-wasm-worker/worker.d.ts +1 -0
- package/dist/esbuild-wasm-worker/worker.js +4 -0
- package/dist/forge-controller.d.ts +38 -0
- package/dist/forge-controller.js +79 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +88 -0
- package/dist/package-resolution/cdn-source.d.ts +11 -0
- package/dist/package-resolution/cdn-source.js +164 -0
- package/dist/package-resolution/diagnostics.d.ts +15 -0
- package/dist/package-resolution/diagnostics.js +51 -0
- package/dist/package-resolution/errors.d.ts +5 -0
- package/dist/package-resolution/errors.js +11 -0
- package/dist/package-resolution/index.d.ts +6 -0
- package/dist/package-resolution/index.js +23 -0
- package/dist/package-resolution/manifest.d.ts +5 -0
- package/dist/package-resolution/manifest.js +128 -0
- package/dist/package-resolution/resolver.d.ts +17 -0
- package/dist/package-resolution/resolver.js +172 -0
- package/dist/package-resolution/types.d.ts +98 -0
- package/dist/package-resolution/types.js +4 -0
- package/dist/terminal-commands/diagnostics.d.ts +3 -0
- package/dist/terminal-commands/diagnostics.js +21 -0
- package/dist/terminal-commands/index.d.ts +7 -0
- package/dist/terminal-commands/index.js +22 -0
- package/dist/terminal-commands/npm-build.d.ts +7 -0
- package/dist/terminal-commands/npm-build.js +63 -0
- package/dist/terminal-commands/npm-dev.d.ts +7 -0
- package/dist/terminal-commands/npm-dev.js +68 -0
- package/dist/terminal-commands/npm-install.d.ts +8 -0
- package/dist/terminal-commands/npm-install.js +56 -0
- package/dist/terminal-commands/parser.d.ts +2 -0
- package/dist/terminal-commands/parser.js +52 -0
- package/dist/terminal-commands/terminal.d.ts +12 -0
- package/dist/terminal-commands/terminal.js +73 -0
- package/dist/terminal-commands/types.d.ts +65 -0
- package/dist/terminal-commands/types.js +4 -0
- package/dist/virtual-file-system/diagnostics.d.ts +14 -0
- package/dist/virtual-file-system/diagnostics.js +55 -0
- package/dist/virtual-file-system/errors.d.ts +5 -0
- package/dist/virtual-file-system/errors.js +11 -0
- package/dist/virtual-file-system/index.d.ts +26 -0
- package/dist/virtual-file-system/index.js +47 -0
- package/dist/virtual-file-system/limits.d.ts +4 -0
- package/dist/virtual-file-system/limits.js +33 -0
- package/dist/virtual-file-system/memory.d.ts +33 -0
- package/dist/virtual-file-system/memory.js +551 -0
- package/dist/virtual-file-system/path.d.ts +9 -0
- package/dist/virtual-file-system/path.js +47 -0
- package/dist/virtual-file-system/storage.d.ts +32 -0
- package/dist/virtual-file-system/storage.js +194 -0
- package/dist/virtual-file-system/types.d.ts +165 -0
- package/dist/virtual-file-system/types.js +2 -0
- package/package.json +43 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InMemoryVirtualFileSystem = void 0;
|
|
4
|
+
const diagnostics_1 = require("./diagnostics");
|
|
5
|
+
const errors_1 = require("./errors");
|
|
6
|
+
const limits_1 = require("./limits");
|
|
7
|
+
const path_1 = require("./path");
|
|
8
|
+
class InMemoryVirtualFileSystem {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.capability = "virtual-file-system";
|
|
11
|
+
this.watchers = new Set();
|
|
12
|
+
const now = timestamp();
|
|
13
|
+
this.workspaceId = options.workspaceId ?? "default";
|
|
14
|
+
this.workspaceName = options.workspaceName ?? "Untitled Workspace";
|
|
15
|
+
this.lock = options.lock;
|
|
16
|
+
this.storage = options.storage;
|
|
17
|
+
this.limits = (0, limits_1.resolveForgeVirtualFileSystemLimits)(options.limits);
|
|
18
|
+
this.metadata = options.metadata ?? {
|
|
19
|
+
createdAt: now,
|
|
20
|
+
updatedAt: now,
|
|
21
|
+
};
|
|
22
|
+
this.records = createInitialRecords(now, options.files);
|
|
23
|
+
}
|
|
24
|
+
async readFile(path) {
|
|
25
|
+
const record = this.requireFile(path);
|
|
26
|
+
return copyBytes(record.content ?? new Uint8Array());
|
|
27
|
+
}
|
|
28
|
+
async readText(path) {
|
|
29
|
+
return new TextDecoder().decode(await this.readFile(path));
|
|
30
|
+
}
|
|
31
|
+
async writeFile(path, content, options = {}) {
|
|
32
|
+
const normalizedPath = this.normalize(path);
|
|
33
|
+
const parentPath = getParentPath(normalizedPath);
|
|
34
|
+
const parent = this.records.get(parentPath);
|
|
35
|
+
if (parent === undefined) {
|
|
36
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createMissingParentDiagnostic)(path));
|
|
37
|
+
}
|
|
38
|
+
if (parent.kind !== "directory") {
|
|
39
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createForgeDiagnostic)({
|
|
40
|
+
code: "not_directory",
|
|
41
|
+
message: "Parent path is not a directory.",
|
|
42
|
+
path: parentPath,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
const existing = this.records.get(normalizedPath);
|
|
46
|
+
if (existing?.kind === "directory") {
|
|
47
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createForgeDiagnostic)({
|
|
48
|
+
code: "is_directory",
|
|
49
|
+
message: "Cannot write file content to a directory.",
|
|
50
|
+
path: normalizedPath,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
const bytes = toBytes(content);
|
|
54
|
+
this.assertLimit("fileSize", bytes.byteLength, this.limits.maxFileSizeBytes, normalizedPath);
|
|
55
|
+
const now = timestamp();
|
|
56
|
+
const next = cloneRecords(this.records);
|
|
57
|
+
const nextRecord = {
|
|
58
|
+
path: normalizedPath,
|
|
59
|
+
kind: "file",
|
|
60
|
+
content: copyBytes(bytes),
|
|
61
|
+
contentType: options.contentType,
|
|
62
|
+
contentHash: hashBytes(bytes),
|
|
63
|
+
size: bytes.byteLength,
|
|
64
|
+
createdAt: existing?.createdAt ?? now,
|
|
65
|
+
updatedAt: now,
|
|
66
|
+
source: options.source ?? existing?.source ?? "user",
|
|
67
|
+
};
|
|
68
|
+
next.set(normalizedPath, nextRecord);
|
|
69
|
+
await this.commit(next, [
|
|
70
|
+
existing === undefined
|
|
71
|
+
? {
|
|
72
|
+
type: "file:created",
|
|
73
|
+
path: normalizedPath,
|
|
74
|
+
stat: toStat(nextRecord),
|
|
75
|
+
}
|
|
76
|
+
: {
|
|
77
|
+
type: "file:updated",
|
|
78
|
+
path: normalizedPath,
|
|
79
|
+
stat: toStat(nextRecord),
|
|
80
|
+
previousHash: existing.contentHash,
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
async delete(path, options = {}) {
|
|
85
|
+
const normalizedPath = this.normalize(path);
|
|
86
|
+
if (normalizedPath === "/") {
|
|
87
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createInvalidPathDiagnostic)(path, "Cannot delete the workspace root."));
|
|
88
|
+
}
|
|
89
|
+
const deletedRecord = this.requireRecord(normalizedPath);
|
|
90
|
+
const descendantPrefix = `${normalizedPath}/`;
|
|
91
|
+
const descendants = Array.from(this.records.keys()).filter((recordPath) => recordPath.startsWith(descendantPrefix));
|
|
92
|
+
if (descendants.length > 0 && options.recursive !== true) {
|
|
93
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createForgeDiagnostic)({
|
|
94
|
+
code: "directory_not_empty",
|
|
95
|
+
message: "Directory is not empty.",
|
|
96
|
+
path: normalizedPath,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
const next = cloneRecords(this.records);
|
|
100
|
+
next.delete(normalizedPath);
|
|
101
|
+
for (const descendant of descendants) {
|
|
102
|
+
next.delete(descendant);
|
|
103
|
+
}
|
|
104
|
+
await this.commit(next, [
|
|
105
|
+
{
|
|
106
|
+
type: "file:deleted",
|
|
107
|
+
path: normalizedPath,
|
|
108
|
+
stat: toStat(deletedRecord),
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
async rename(from, to) {
|
|
113
|
+
const normalizedFrom = this.normalize(from);
|
|
114
|
+
const normalizedTo = this.normalize(to);
|
|
115
|
+
if (normalizedFrom === "/" || normalizedTo === "/") {
|
|
116
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createInvalidPathDiagnostic)(from, "Cannot rename the workspace root."));
|
|
117
|
+
}
|
|
118
|
+
if (normalizedTo.startsWith(`${normalizedFrom}/`)) {
|
|
119
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createInvalidPathDiagnostic)(to, "Cannot rename a directory into one of its descendants."));
|
|
120
|
+
}
|
|
121
|
+
const source = this.requireRecord(normalizedFrom);
|
|
122
|
+
const targetParentPath = getParentPath(normalizedTo);
|
|
123
|
+
const targetParent = this.records.get(targetParentPath);
|
|
124
|
+
if (targetParent === undefined) {
|
|
125
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createMissingParentDiagnostic)(to));
|
|
126
|
+
}
|
|
127
|
+
if (targetParent.kind !== "directory") {
|
|
128
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createForgeDiagnostic)({
|
|
129
|
+
code: "not_directory",
|
|
130
|
+
message: "Target parent path is not a directory.",
|
|
131
|
+
path: targetParentPath,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
if (this.records.has(normalizedTo)) {
|
|
135
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createForgeDiagnostic)({
|
|
136
|
+
code: "already_exists",
|
|
137
|
+
message: "Target path already exists.",
|
|
138
|
+
path: normalizedTo,
|
|
139
|
+
relatedPath: normalizedFrom,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
const now = timestamp();
|
|
143
|
+
const next = cloneRecords(this.records);
|
|
144
|
+
const prefix = `${normalizedFrom}/`;
|
|
145
|
+
const movedEntries = [];
|
|
146
|
+
for (const [recordPath, record] of this.records) {
|
|
147
|
+
if (recordPath === normalizedFrom || recordPath.startsWith(prefix)) {
|
|
148
|
+
const suffix = recordPath.slice(normalizedFrom.length);
|
|
149
|
+
const movedPath = `${normalizedTo}${suffix}`;
|
|
150
|
+
if (recordPath !== normalizedFrom && this.records.has(movedPath)) {
|
|
151
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createForgeDiagnostic)({
|
|
152
|
+
code: "already_exists",
|
|
153
|
+
message: "Renamed path would overwrite an existing record.",
|
|
154
|
+
path: movedPath,
|
|
155
|
+
relatedPath: normalizedFrom,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
movedEntries.push([
|
|
159
|
+
movedPath,
|
|
160
|
+
{
|
|
161
|
+
...record,
|
|
162
|
+
path: movedPath,
|
|
163
|
+
updatedAt: now,
|
|
164
|
+
content: copyOptionalBytes(record.content),
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
next.delete(recordPath);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
next.set(normalizedTo, {
|
|
171
|
+
...source,
|
|
172
|
+
path: normalizedTo,
|
|
173
|
+
updatedAt: now,
|
|
174
|
+
content: copyOptionalBytes(source.content),
|
|
175
|
+
});
|
|
176
|
+
for (const [movedPath, movedRecord] of movedEntries) {
|
|
177
|
+
next.set(movedPath, movedRecord);
|
|
178
|
+
}
|
|
179
|
+
const renamedRecord = next.get(normalizedTo);
|
|
180
|
+
await this.commit(next, [
|
|
181
|
+
{
|
|
182
|
+
type: "file:renamed",
|
|
183
|
+
from: normalizedFrom,
|
|
184
|
+
to: normalizedTo,
|
|
185
|
+
stat: toStat(renamedRecord ?? source),
|
|
186
|
+
},
|
|
187
|
+
]);
|
|
188
|
+
}
|
|
189
|
+
async mkdir(path, options = {}) {
|
|
190
|
+
const normalizedPath = this.normalize(path);
|
|
191
|
+
if (normalizedPath === "/" || this.records.has(normalizedPath)) {
|
|
192
|
+
const existing = this.records.get(normalizedPath);
|
|
193
|
+
if (existing?.kind === "directory") {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createForgeDiagnostic)({
|
|
197
|
+
code: "already_exists",
|
|
198
|
+
message: "Path already exists and is not a directory.",
|
|
199
|
+
path: normalizedPath,
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
const missingParents = collectMissingParents(normalizedPath, this.records);
|
|
203
|
+
if (missingParents.length > 0 && options.recursive !== true) {
|
|
204
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createMissingParentDiagnostic)(path));
|
|
205
|
+
}
|
|
206
|
+
const now = timestamp();
|
|
207
|
+
const next = cloneRecords(this.records);
|
|
208
|
+
const directories = [...missingParents, normalizedPath];
|
|
209
|
+
const events = [];
|
|
210
|
+
for (const directoryPath of directories) {
|
|
211
|
+
if (!next.has(directoryPath)) {
|
|
212
|
+
const nextRecord = {
|
|
213
|
+
path: directoryPath,
|
|
214
|
+
kind: "directory",
|
|
215
|
+
size: 0,
|
|
216
|
+
createdAt: now,
|
|
217
|
+
updatedAt: now,
|
|
218
|
+
source: options.source ?? "user",
|
|
219
|
+
};
|
|
220
|
+
next.set(directoryPath, nextRecord);
|
|
221
|
+
events.push({
|
|
222
|
+
type: "file:created",
|
|
223
|
+
path: directoryPath,
|
|
224
|
+
stat: toStat(nextRecord),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
await this.commit(next, events);
|
|
229
|
+
}
|
|
230
|
+
async readdir(path) {
|
|
231
|
+
const normalizedPath = this.normalize(path);
|
|
232
|
+
const directory = this.requireRecord(normalizedPath);
|
|
233
|
+
if (directory.kind !== "directory") {
|
|
234
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createForgeDiagnostic)({
|
|
235
|
+
code: "not_directory",
|
|
236
|
+
message: "Path is not a directory.",
|
|
237
|
+
path: normalizedPath,
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
const entries = [];
|
|
241
|
+
const prefix = normalizedPath === "/" ? "/" : `${normalizedPath}/`;
|
|
242
|
+
for (const [recordPath, record] of this.records) {
|
|
243
|
+
if (recordPath === normalizedPath || !recordPath.startsWith(prefix)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const relativePath = recordPath.slice(prefix.length);
|
|
247
|
+
if (relativePath.includes("/")) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
entries.push({
|
|
251
|
+
name: relativePath,
|
|
252
|
+
path: recordPath,
|
|
253
|
+
kind: record.kind,
|
|
254
|
+
stat: toStat(record),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
258
|
+
}
|
|
259
|
+
async stat(path) {
|
|
260
|
+
return toStat(this.requireRecord(this.normalize(path)));
|
|
261
|
+
}
|
|
262
|
+
async exists(path) {
|
|
263
|
+
const normalizedPath = this.normalize(path);
|
|
264
|
+
return this.records.has(normalizedPath);
|
|
265
|
+
}
|
|
266
|
+
async snapshot(options = {}) {
|
|
267
|
+
return {
|
|
268
|
+
id: `snapshot-${Date.now()}`,
|
|
269
|
+
workspaceId: this.workspaceId,
|
|
270
|
+
createdAt: timestamp(),
|
|
271
|
+
files: recordsToFileRecords(this.records).filter((file) => shouldIncludeInSnapshot(file, options)),
|
|
272
|
+
lock: options.includeRuntimeMetadata === false ? undefined : this.lock,
|
|
273
|
+
metadata: {
|
|
274
|
+
workspaceName: this.workspaceName,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
async restore(snapshot, _options) {
|
|
279
|
+
const now = timestamp();
|
|
280
|
+
const next = createInitialRecords(now);
|
|
281
|
+
for (const record of this.records.values()) {
|
|
282
|
+
if (record.path !== "/" && record.source === "cache") {
|
|
283
|
+
next.set(record.path, {
|
|
284
|
+
...record,
|
|
285
|
+
content: copyOptionalBytes(record.content),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const file of snapshot.files) {
|
|
290
|
+
if (file.path === "/" || file.source === "cache") {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const normalizedPath = this.normalize(file.path);
|
|
294
|
+
next.set(normalizedPath, {
|
|
295
|
+
path: normalizedPath,
|
|
296
|
+
kind: file.kind,
|
|
297
|
+
content: copyOptionalBytes(file.content),
|
|
298
|
+
contentType: file.contentType,
|
|
299
|
+
contentHash: file.contentHash,
|
|
300
|
+
size: file.size,
|
|
301
|
+
createdAt: file.createdAt,
|
|
302
|
+
updatedAt: file.updatedAt,
|
|
303
|
+
source: file.source,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
await this.commit(next, createRestoreEvents(this.records, next));
|
|
307
|
+
}
|
|
308
|
+
watch(listener) {
|
|
309
|
+
this.watchers.add(listener);
|
|
310
|
+
return () => {
|
|
311
|
+
this.watchers.delete(listener);
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
normalize(path) {
|
|
315
|
+
const result = (0, path_1.normalizeForgePath)(path);
|
|
316
|
+
if (result.ok) {
|
|
317
|
+
return result.path;
|
|
318
|
+
}
|
|
319
|
+
throw new errors_1.ForgeFileSystemError(result.diagnostic);
|
|
320
|
+
}
|
|
321
|
+
requireRecord(path) {
|
|
322
|
+
const record = this.records.get(path);
|
|
323
|
+
if (record === undefined) {
|
|
324
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createNotFoundDiagnostic)(path));
|
|
325
|
+
}
|
|
326
|
+
return record;
|
|
327
|
+
}
|
|
328
|
+
requireFile(path) {
|
|
329
|
+
const normalizedPath = this.normalize(path);
|
|
330
|
+
const record = this.requireRecord(normalizedPath);
|
|
331
|
+
if (record.kind !== "file") {
|
|
332
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createForgeDiagnostic)({
|
|
333
|
+
code: "not_file",
|
|
334
|
+
message: "Path is not a file.",
|
|
335
|
+
path: normalizedPath,
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
return record;
|
|
339
|
+
}
|
|
340
|
+
async commit(next, events = []) {
|
|
341
|
+
this.assertWorkspaceLimits(next);
|
|
342
|
+
const metadata = {
|
|
343
|
+
...this.metadata,
|
|
344
|
+
updatedAt: timestamp(),
|
|
345
|
+
};
|
|
346
|
+
if (this.storage !== undefined) {
|
|
347
|
+
await this.storage.saveWorkspace({
|
|
348
|
+
id: this.workspaceId,
|
|
349
|
+
name: this.workspaceName,
|
|
350
|
+
files: recordsToFileRecords(next).filter((file) => file.source !== "cache"),
|
|
351
|
+
metadata,
|
|
352
|
+
lock: this.lock,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
this.records = next;
|
|
356
|
+
this.metadata = metadata;
|
|
357
|
+
this.emit(events);
|
|
358
|
+
}
|
|
359
|
+
assertWorkspaceLimits(records) {
|
|
360
|
+
this.assertLimit("workspaceSize", workspaceSize(records), this.limits.maxWorkspaceSizeBytes);
|
|
361
|
+
this.assertLimit("fileCount", fileCount(records), this.limits.maxFileCount);
|
|
362
|
+
}
|
|
363
|
+
assertLimit(name, actual, maximum, path) {
|
|
364
|
+
if (actual > maximum) {
|
|
365
|
+
throw new errors_1.ForgeFileSystemError((0, limits_1.createLimitExceededDiagnostic)(name, actual, maximum, path));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
emit(events) {
|
|
369
|
+
for (const event of events) {
|
|
370
|
+
for (const watcher of this.watchers) {
|
|
371
|
+
try {
|
|
372
|
+
watcher(event);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// Watchers are observers; one listener must not roll back a commit.
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
exports.InMemoryVirtualFileSystem = InMemoryVirtualFileSystem;
|
|
382
|
+
function createInitialRecords(createdAt, files = []) {
|
|
383
|
+
const records = new Map([
|
|
384
|
+
[
|
|
385
|
+
"/",
|
|
386
|
+
{
|
|
387
|
+
path: "/",
|
|
388
|
+
kind: "directory",
|
|
389
|
+
size: 0,
|
|
390
|
+
createdAt,
|
|
391
|
+
updatedAt: createdAt,
|
|
392
|
+
source: "user",
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
]);
|
|
396
|
+
for (const file of files) {
|
|
397
|
+
if (file.path === "/") {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
records.set(file.path, {
|
|
401
|
+
path: file.path,
|
|
402
|
+
kind: file.kind,
|
|
403
|
+
content: copyOptionalBytes(file.content),
|
|
404
|
+
contentType: file.contentType,
|
|
405
|
+
contentHash: file.contentHash,
|
|
406
|
+
size: file.size,
|
|
407
|
+
createdAt: file.createdAt,
|
|
408
|
+
updatedAt: file.updatedAt,
|
|
409
|
+
source: file.source,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return records;
|
|
413
|
+
}
|
|
414
|
+
function cloneRecords(records) {
|
|
415
|
+
return new Map(Array.from(records, ([path, record]) => [
|
|
416
|
+
path,
|
|
417
|
+
{
|
|
418
|
+
...record,
|
|
419
|
+
content: copyOptionalBytes(record.content),
|
|
420
|
+
},
|
|
421
|
+
]));
|
|
422
|
+
}
|
|
423
|
+
function collectMissingParents(path, records) {
|
|
424
|
+
const parts = path.split("/").filter(Boolean);
|
|
425
|
+
const missing = [];
|
|
426
|
+
for (let index = 1; index < parts.length; index += 1) {
|
|
427
|
+
const parentPath = `/${parts.slice(0, index).join("/")}`;
|
|
428
|
+
const parent = records.get(parentPath);
|
|
429
|
+
if (parent === undefined) {
|
|
430
|
+
missing.push(parentPath);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (parent.kind !== "directory") {
|
|
434
|
+
throw new errors_1.ForgeFileSystemError((0, diagnostics_1.createForgeDiagnostic)({
|
|
435
|
+
code: "not_directory",
|
|
436
|
+
message: "Parent path is not a directory.",
|
|
437
|
+
path: parentPath,
|
|
438
|
+
}));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return missing;
|
|
442
|
+
}
|
|
443
|
+
function getParentPath(path) {
|
|
444
|
+
const lastSlashIndex = path.lastIndexOf("/");
|
|
445
|
+
return lastSlashIndex <= 0 ? "/" : path.slice(0, lastSlashIndex);
|
|
446
|
+
}
|
|
447
|
+
function toBytes(content) {
|
|
448
|
+
return typeof content === "string"
|
|
449
|
+
? new TextEncoder().encode(content)
|
|
450
|
+
: copyBytes(content);
|
|
451
|
+
}
|
|
452
|
+
function copyBytes(bytes) {
|
|
453
|
+
return new Uint8Array(bytes);
|
|
454
|
+
}
|
|
455
|
+
function copyOptionalBytes(bytes) {
|
|
456
|
+
return bytes === undefined ? undefined : copyBytes(bytes);
|
|
457
|
+
}
|
|
458
|
+
function hashBytes(bytes) {
|
|
459
|
+
let hash = 0x811c9dc5;
|
|
460
|
+
for (const byte of bytes) {
|
|
461
|
+
hash ^= byte;
|
|
462
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
463
|
+
}
|
|
464
|
+
return `fnv1a32:${hash.toString(16).padStart(8, "0")}`;
|
|
465
|
+
}
|
|
466
|
+
function timestamp() {
|
|
467
|
+
return new Date().toISOString();
|
|
468
|
+
}
|
|
469
|
+
function toStat(record) {
|
|
470
|
+
return {
|
|
471
|
+
path: record.path,
|
|
472
|
+
kind: record.kind,
|
|
473
|
+
contentType: record.contentType,
|
|
474
|
+
contentHash: record.contentHash,
|
|
475
|
+
size: record.size,
|
|
476
|
+
createdAt: record.createdAt,
|
|
477
|
+
updatedAt: record.updatedAt,
|
|
478
|
+
source: record.source,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function toFileRecord(record) {
|
|
482
|
+
return {
|
|
483
|
+
...toStat(record),
|
|
484
|
+
content: copyOptionalBytes(record.content),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function recordsToFileRecords(records) {
|
|
488
|
+
return Array.from(records.values(), toFileRecord);
|
|
489
|
+
}
|
|
490
|
+
function workspaceSize(records) {
|
|
491
|
+
let size = 0;
|
|
492
|
+
for (const record of records.values()) {
|
|
493
|
+
if (record.kind === "file" && record.source !== "cache") {
|
|
494
|
+
size += record.size;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return size;
|
|
498
|
+
}
|
|
499
|
+
function fileCount(records) {
|
|
500
|
+
let count = 0;
|
|
501
|
+
for (const record of records.values()) {
|
|
502
|
+
if (record.kind === "file" && record.source !== "cache") {
|
|
503
|
+
count += 1;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return count;
|
|
507
|
+
}
|
|
508
|
+
function shouldIncludeInSnapshot(file, options) {
|
|
509
|
+
if (file.path === "/" || file.source === "cache") {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
if (file.source === "generated" && options.includeGeneratedFiles !== true) {
|
|
513
|
+
return file.path === "/package.json";
|
|
514
|
+
}
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
function createRestoreEvents(previous, next) {
|
|
518
|
+
const events = [];
|
|
519
|
+
for (const [path, record] of previous) {
|
|
520
|
+
if (path !== "/" && record.source !== "cache" && !next.has(path)) {
|
|
521
|
+
events.push({
|
|
522
|
+
type: "file:deleted",
|
|
523
|
+
path,
|
|
524
|
+
stat: toStat(record),
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
for (const [path, record] of next) {
|
|
529
|
+
if (path === "/" || record.source === "cache") {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const previousRecord = previous.get(path);
|
|
533
|
+
if (previousRecord === undefined) {
|
|
534
|
+
events.push({
|
|
535
|
+
type: "file:created",
|
|
536
|
+
path,
|
|
537
|
+
stat: toStat(record),
|
|
538
|
+
});
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (previousRecord.contentHash !== record.contentHash) {
|
|
542
|
+
events.push({
|
|
543
|
+
type: "file:updated",
|
|
544
|
+
path,
|
|
545
|
+
stat: toStat(record),
|
|
546
|
+
previousHash: previousRecord.contentHash,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return events;
|
|
551
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ForgeDiagnostic } from "./types";
|
|
2
|
+
export type ForgePathNormalizationResult = {
|
|
3
|
+
readonly ok: true;
|
|
4
|
+
readonly path: string;
|
|
5
|
+
} | {
|
|
6
|
+
readonly ok: false;
|
|
7
|
+
readonly diagnostic: ForgeDiagnostic;
|
|
8
|
+
};
|
|
9
|
+
export declare function normalizeForgePath(path: string): ForgePathNormalizationResult;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeForgePath = void 0;
|
|
4
|
+
const diagnostics_1 = require("./diagnostics");
|
|
5
|
+
function normalizeForgePath(path) {
|
|
6
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
7
|
+
return {
|
|
8
|
+
ok: false,
|
|
9
|
+
diagnostic: (0, diagnostics_1.createInvalidPathDiagnostic)(String(path), "Path must be a non-empty string."),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (path.includes("\0")) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
diagnostic: (0, diagnostics_1.createInvalidPathDiagnostic)(path, "Path cannot contain null bytes."),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (path.includes("\\")) {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
diagnostic: (0, diagnostics_1.createInvalidPathDiagnostic)(path, "Path must use POSIX forward slashes."),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const absolutePath = path.startsWith("/") ? path : `/${path}`;
|
|
25
|
+
const segments = [];
|
|
26
|
+
for (const segment of absolutePath.split("/")) {
|
|
27
|
+
if (segment.length === 0 || segment === ".") {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (segment === "..") {
|
|
31
|
+
if (segments.length === 0) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
diagnostic: (0, diagnostics_1.createPathTraversalDiagnostic)(path),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
segments.pop();
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
segments.push(segment);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
path: segments.length === 0 ? "/" : `/${segments.join("/")}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
exports.normalizeForgePath = normalizeForgePath;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ForgeBuildCacheRecord, ForgePackageCacheRecord, ForgePersistedWorkspace, ForgeVirtualFileSystemLimits, ForgeVirtualFileSystemStorage } from "./types";
|
|
2
|
+
export declare const FORGE_INDEXED_DB_NAME = "codemine-forge";
|
|
3
|
+
export declare const FORGE_INDEXED_DB_VERSION = 1;
|
|
4
|
+
export declare const FORGE_INDEXED_DB_STORES: {
|
|
5
|
+
readonly buildCache: "build-cache";
|
|
6
|
+
readonly packageCache: "package-cache";
|
|
7
|
+
readonly workspaceFiles: "workspace-files";
|
|
8
|
+
readonly workspaceMetadata: "workspace-metadata";
|
|
9
|
+
readonly workspaceSnapshots: "workspace-snapshots";
|
|
10
|
+
};
|
|
11
|
+
export interface IndexedDbForgeStorageOptions {
|
|
12
|
+
readonly databaseName?: string;
|
|
13
|
+
readonly limits?: ForgeVirtualFileSystemLimits;
|
|
14
|
+
}
|
|
15
|
+
export declare class IndexedDbForgeStorage implements ForgeVirtualFileSystemStorage {
|
|
16
|
+
private readonly databaseName;
|
|
17
|
+
private readonly limits;
|
|
18
|
+
private databasePromise;
|
|
19
|
+
constructor(options?: IndexedDbForgeStorageOptions);
|
|
20
|
+
loadWorkspace(workspaceId: string): Promise<ForgePersistedWorkspace | undefined>;
|
|
21
|
+
saveWorkspace(workspace: ForgePersistedWorkspace): Promise<void>;
|
|
22
|
+
getPackageCacheRecord(key: string): Promise<ForgePackageCacheRecord | undefined>;
|
|
23
|
+
putPackageCacheRecord(record: ForgePackageCacheRecord): Promise<void>;
|
|
24
|
+
getBuildCacheRecord(key: string): Promise<ForgeBuildCacheRecord | undefined>;
|
|
25
|
+
putBuildCacheRecord(record: ForgeBuildCacheRecord): Promise<void>;
|
|
26
|
+
clearBuildCache(): Promise<void>;
|
|
27
|
+
private getCacheRecord;
|
|
28
|
+
private putCacheRecord;
|
|
29
|
+
private assertCacheLimit;
|
|
30
|
+
private openDatabase;
|
|
31
|
+
}
|
|
32
|
+
export declare function createIndexedDbForgeStorage(options?: IndexedDbForgeStorageOptions): IndexedDbForgeStorage;
|