@dexto/tools-filesystem 1.5.8 → 1.6.1
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/directory-approval.cjs +98 -0
- package/dist/directory-approval.d.ts +24 -0
- package/dist/directory-approval.d.ts.map +1 -0
- package/dist/directory-approval.integration.test.cjs +175 -390
- package/dist/directory-approval.integration.test.d.ts +14 -2
- package/dist/directory-approval.integration.test.d.ts.map +1 -0
- package/dist/directory-approval.integration.test.js +178 -390
- package/dist/directory-approval.js +63 -0
- package/dist/edit-file-tool.cjs +109 -120
- package/dist/edit-file-tool.d.ts +22 -9
- package/dist/edit-file-tool.d.ts.map +1 -0
- package/dist/edit-file-tool.js +116 -110
- package/dist/edit-file-tool.test.cjs +109 -29
- package/dist/edit-file-tool.test.d.ts +7 -2
- package/dist/edit-file-tool.test.d.ts.map +1 -0
- package/dist/edit-file-tool.test.js +109 -29
- package/dist/error-codes.cjs +4 -0
- package/dist/error-codes.d.ts +6 -3
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +4 -0
- package/dist/errors.cjs +48 -0
- package/dist/errors.d.ts +20 -7
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +48 -0
- package/dist/file-tool-types.d.ts +8 -40
- package/dist/file-tool-types.d.ts.map +1 -0
- package/dist/filesystem-service.cjs +325 -10
- package/dist/filesystem-service.d.ts +41 -12
- package/dist/filesystem-service.d.ts.map +1 -0
- package/dist/filesystem-service.js +326 -11
- package/dist/filesystem-service.test.cjs +10 -2
- package/dist/filesystem-service.test.d.ts +7 -2
- package/dist/filesystem-service.test.d.ts.map +1 -0
- package/dist/filesystem-service.test.js +10 -2
- package/dist/glob-files-tool.cjs +32 -46
- package/dist/glob-files-tool.d.ts +19 -9
- package/dist/glob-files-tool.d.ts.map +1 -0
- package/dist/glob-files-tool.js +33 -47
- package/dist/grep-content-tool.cjs +40 -45
- package/dist/grep-content-tool.d.ts +28 -9
- package/dist/grep-content-tool.d.ts.map +1 -0
- package/dist/grep-content-tool.js +41 -46
- package/dist/index.cjs +6 -3
- package/dist/index.d.cts +852 -14
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -2
- package/dist/path-validator.cjs +28 -2
- package/dist/path-validator.d.ts +20 -9
- package/dist/path-validator.d.ts.map +1 -0
- package/dist/path-validator.js +28 -2
- package/dist/path-validator.test.d.ts +7 -2
- package/dist/path-validator.test.d.ts.map +1 -0
- package/dist/read-file-tool.cjs +26 -59
- package/dist/read-file-tool.d.ts +19 -9
- package/dist/read-file-tool.d.ts.map +1 -0
- package/dist/read-file-tool.js +27 -50
- package/dist/tool-factory-config.cjs +61 -0
- package/dist/{tool-provider.d.ts → tool-factory-config.d.ts} +13 -30
- package/dist/tool-factory-config.d.ts.map +1 -0
- package/dist/tool-factory-config.js +36 -0
- package/dist/tool-factory.cjs +123 -0
- package/dist/tool-factory.d.ts +4 -0
- package/dist/tool-factory.d.ts.map +1 -0
- package/dist/tool-factory.js +102 -0
- package/dist/types.d.ts +82 -18
- package/dist/types.d.ts.map +1 -0
- package/dist/write-file-tool.cjs +93 -99
- package/dist/write-file-tool.d.ts +22 -9
- package/dist/write-file-tool.d.ts.map +1 -0
- package/dist/write-file-tool.js +97 -91
- package/dist/write-file-tool.test.cjs +139 -33
- package/dist/write-file-tool.test.d.ts +7 -2
- package/dist/write-file-tool.test.d.ts.map +1 -0
- package/dist/write-file-tool.test.js +139 -33
- package/package.json +5 -4
- package/dist/directory-approval.integration.test.d.cts +0 -2
- package/dist/edit-file-tool.d.cts +0 -17
- package/dist/edit-file-tool.test.d.cts +0 -2
- package/dist/error-codes.d.cts +0 -32
- package/dist/errors.d.cts +0 -112
- package/dist/file-tool-types.d.cts +0 -46
- package/dist/filesystem-service.d.cts +0 -112
- package/dist/filesystem-service.test.d.cts +0 -2
- package/dist/glob-files-tool.d.cts +0 -17
- package/dist/grep-content-tool.d.cts +0 -17
- package/dist/path-validator.d.cts +0 -97
- package/dist/path-validator.test.d.cts +0 -2
- package/dist/read-file-tool.d.cts +0 -17
- package/dist/tool-provider.cjs +0 -123
- package/dist/tool-provider.d.cts +0 -77
- package/dist/tool-provider.js +0 -99
- package/dist/types.d.cts +0 -178
- package/dist/write-file-tool.d.cts +0 -17
- package/dist/write-file-tool.test.d.cts +0 -2
|
@@ -2,22 +2,25 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { glob } from "glob";
|
|
4
4
|
import safeRegex from "safe-regex";
|
|
5
|
-
import { getDextoPath, DextoLogComponent } from "@dexto/core";
|
|
5
|
+
import { DextoRuntimeError, getDextoPath, DextoLogComponent } from "@dexto/core";
|
|
6
6
|
import { PathValidator } from "./path-validator.js";
|
|
7
7
|
import { FileSystemError } from "./errors.js";
|
|
8
8
|
const DEFAULT_ENCODING = "utf-8";
|
|
9
9
|
const DEFAULT_MAX_RESULTS = 1e3;
|
|
10
10
|
const DEFAULT_MAX_SEARCH_RESULTS = 100;
|
|
11
|
+
const DEFAULT_MAX_LIST_RESULTS = 5e3;
|
|
12
|
+
const DEFAULT_LIST_CONCURRENCY = 16;
|
|
11
13
|
class FileSystemService {
|
|
12
14
|
config;
|
|
13
15
|
pathValidator;
|
|
14
16
|
initialized = false;
|
|
15
17
|
initPromise = null;
|
|
16
18
|
logger;
|
|
19
|
+
directoryApprovalChecker;
|
|
17
20
|
/**
|
|
18
21
|
* Create a new FileSystemService with validated configuration.
|
|
19
22
|
*
|
|
20
|
-
* @param config - Fully-validated configuration from
|
|
23
|
+
* @param config - Fully-validated configuration from the factory schema.
|
|
21
24
|
* All required fields have values, defaults already applied.
|
|
22
25
|
* @param logger - Logger instance for this service
|
|
23
26
|
*/
|
|
@@ -91,8 +94,24 @@ class FileSystemService {
|
|
|
91
94
|
* @param checker Function that returns true if path is in an approved directory
|
|
92
95
|
*/
|
|
93
96
|
setDirectoryApprovalChecker(checker) {
|
|
97
|
+
this.directoryApprovalChecker = checker;
|
|
94
98
|
this.pathValidator.setDirectoryApprovalChecker(checker);
|
|
95
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Update the working directory at runtime (e.g., when workspace changes).
|
|
102
|
+
* Rebuilds the PathValidator so allowed/blocked path roots are recalculated.
|
|
103
|
+
*/
|
|
104
|
+
setWorkingDirectory(workingDirectory) {
|
|
105
|
+
const normalized = workingDirectory?.trim();
|
|
106
|
+
if (!normalized) return;
|
|
107
|
+
if (this.config.workingDirectory === normalized) return;
|
|
108
|
+
this.config = { ...this.config, workingDirectory: normalized };
|
|
109
|
+
this.pathValidator = new PathValidator(this.config, this.logger);
|
|
110
|
+
if (this.directoryApprovalChecker) {
|
|
111
|
+
this.pathValidator.setDirectoryApprovalChecker(this.directoryApprovalChecker);
|
|
112
|
+
}
|
|
113
|
+
this.logger.info(`FileSystemService working directory set to ${normalized}`);
|
|
114
|
+
}
|
|
96
115
|
/**
|
|
97
116
|
* Check if a file path is within the configured allowed paths (config only).
|
|
98
117
|
* This is used by file tools to determine if directory approval is needed.
|
|
@@ -103,16 +122,14 @@ class FileSystemService {
|
|
|
103
122
|
async isPathWithinConfigAllowed(filePath) {
|
|
104
123
|
return this.pathValidator.isPathWithinAllowed(filePath);
|
|
105
124
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
*/
|
|
109
|
-
async readFile(filePath, options = {}) {
|
|
110
|
-
await this.ensureInitialized();
|
|
111
|
-
const validation = await this.pathValidator.validatePath(filePath);
|
|
125
|
+
async validateReadPath(filePath, mode) {
|
|
126
|
+
const validation = mode === "toolPreview" ? await this.pathValidator.validatePathForPreview(filePath) : await this.pathValidator.validatePath(filePath);
|
|
112
127
|
if (!validation.isValid || !validation.normalizedPath) {
|
|
113
128
|
throw FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
|
|
114
129
|
}
|
|
115
|
-
|
|
130
|
+
return validation.normalizedPath;
|
|
131
|
+
}
|
|
132
|
+
async readNormalizedFile(normalizedPath, options = {}) {
|
|
116
133
|
try {
|
|
117
134
|
const stats = await fs.stat(normalizedPath);
|
|
118
135
|
if (!stats.isFile()) {
|
|
@@ -126,12 +143,18 @@ class FileSystemService {
|
|
|
126
143
|
);
|
|
127
144
|
}
|
|
128
145
|
} catch (error) {
|
|
146
|
+
if (error instanceof DextoRuntimeError && error.scope === "filesystem") {
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
129
149
|
if (error.code === "ENOENT") {
|
|
130
150
|
throw FileSystemError.fileNotFound(normalizedPath);
|
|
131
151
|
}
|
|
132
152
|
if (error.code === "EACCES") {
|
|
133
153
|
throw FileSystemError.permissionDenied(normalizedPath, "read");
|
|
134
154
|
}
|
|
155
|
+
if (error instanceof DextoRuntimeError) {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
135
158
|
throw FileSystemError.readFailed(
|
|
136
159
|
normalizedPath,
|
|
137
160
|
error instanceof Error ? error.message : String(error)
|
|
@@ -153,20 +176,44 @@ class FileSystemService {
|
|
|
153
176
|
} else {
|
|
154
177
|
selectedLines = lines;
|
|
155
178
|
}
|
|
179
|
+
const returnedContent = selectedLines.join("\n");
|
|
156
180
|
return {
|
|
157
|
-
content:
|
|
181
|
+
content: returnedContent,
|
|
158
182
|
lines: selectedLines.length,
|
|
159
183
|
encoding,
|
|
160
184
|
truncated,
|
|
161
|
-
size: Buffer.byteLength(
|
|
185
|
+
size: Buffer.byteLength(returnedContent, encoding)
|
|
162
186
|
};
|
|
163
187
|
} catch (error) {
|
|
188
|
+
if (error instanceof DextoRuntimeError && error.scope === "filesystem") {
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
164
191
|
throw FileSystemError.readFailed(
|
|
165
192
|
normalizedPath,
|
|
166
193
|
error instanceof Error ? error.message : String(error)
|
|
167
194
|
);
|
|
168
195
|
}
|
|
169
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Read a file with validation and size limits
|
|
199
|
+
*/
|
|
200
|
+
async readFile(filePath, options = {}) {
|
|
201
|
+
await this.ensureInitialized();
|
|
202
|
+
const normalizedPath = await this.validateReadPath(filePath, "execute");
|
|
203
|
+
return await this.readNormalizedFile(normalizedPath, options);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Preview-only file read that bypasses config-allowed roots.
|
|
207
|
+
*
|
|
208
|
+
* This is intended for UI previews (diffs, create previews) shown BEFORE a user
|
|
209
|
+
* confirms directory access for the tool call. The returned content is UI-only
|
|
210
|
+
* and should not be forwarded to the LLM.
|
|
211
|
+
*/
|
|
212
|
+
async readFileForToolPreview(filePath, options = {}) {
|
|
213
|
+
await this.ensureInitialized();
|
|
214
|
+
const normalizedPath = await this.validateReadPath(filePath, "toolPreview");
|
|
215
|
+
return await this.readNormalizedFile(normalizedPath, options);
|
|
216
|
+
}
|
|
170
217
|
/**
|
|
171
218
|
* Find files matching a glob pattern
|
|
172
219
|
*/
|
|
@@ -229,6 +276,274 @@ class FileSystemService {
|
|
|
229
276
|
);
|
|
230
277
|
}
|
|
231
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* List contents of a directory (non-recursive)
|
|
281
|
+
*/
|
|
282
|
+
async listDirectory(dirPath, options = {}) {
|
|
283
|
+
await this.ensureInitialized();
|
|
284
|
+
const validation = await this.pathValidator.validatePath(dirPath);
|
|
285
|
+
if (!validation.isValid || !validation.normalizedPath) {
|
|
286
|
+
throw FileSystemError.invalidPath(dirPath, validation.error || "Unknown error");
|
|
287
|
+
}
|
|
288
|
+
const normalizedPath = validation.normalizedPath;
|
|
289
|
+
try {
|
|
290
|
+
const stats = await fs.stat(normalizedPath);
|
|
291
|
+
if (!stats.isDirectory()) {
|
|
292
|
+
throw FileSystemError.invalidPath(normalizedPath, "Path is not a directory");
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (error instanceof DextoRuntimeError && error.scope === "filesystem") {
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
if (error.code === "ENOENT") {
|
|
299
|
+
throw FileSystemError.directoryNotFound(normalizedPath);
|
|
300
|
+
}
|
|
301
|
+
if (error.code === "EACCES") {
|
|
302
|
+
throw FileSystemError.permissionDenied(normalizedPath, "read");
|
|
303
|
+
}
|
|
304
|
+
throw FileSystemError.listFailed(
|
|
305
|
+
normalizedPath,
|
|
306
|
+
error instanceof Error ? error.message : String(error)
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
const includeHidden = options.includeHidden ?? true;
|
|
310
|
+
const includeMetadata = options.includeMetadata !== false;
|
|
311
|
+
const maxEntries = options.maxEntries ?? DEFAULT_MAX_LIST_RESULTS;
|
|
312
|
+
try {
|
|
313
|
+
const dirEntries = await fs.readdir(normalizedPath, { withFileTypes: true });
|
|
314
|
+
const candidates = dirEntries.filter(
|
|
315
|
+
(entry) => includeHidden || !entry.name.startsWith(".")
|
|
316
|
+
);
|
|
317
|
+
const concurrency = DEFAULT_LIST_CONCURRENCY;
|
|
318
|
+
const validatedEntries = await this.mapWithConcurrency(
|
|
319
|
+
candidates,
|
|
320
|
+
concurrency,
|
|
321
|
+
async (entry) => {
|
|
322
|
+
const entryPath = path.join(normalizedPath, entry.name);
|
|
323
|
+
const entryValidation = await this.pathValidator.validatePath(entryPath);
|
|
324
|
+
if (!entryValidation.isValid || !entryValidation.normalizedPath) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
entry,
|
|
329
|
+
normalizedPath: entryValidation.normalizedPath
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
const validEntries = validatedEntries.filter(Boolean);
|
|
334
|
+
if (maxEntries <= 0) {
|
|
335
|
+
return {
|
|
336
|
+
path: normalizedPath,
|
|
337
|
+
entries: [],
|
|
338
|
+
truncated: validEntries.length > 0,
|
|
339
|
+
totalEntries: validEntries.length
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (!includeMetadata) {
|
|
343
|
+
const entries2 = validEntries.slice(0, maxEntries).map((entry) => ({
|
|
344
|
+
name: entry.entry.name,
|
|
345
|
+
path: entry.normalizedPath,
|
|
346
|
+
isDirectory: entry.entry.isDirectory(),
|
|
347
|
+
size: 0,
|
|
348
|
+
modified: /* @__PURE__ */ new Date()
|
|
349
|
+
}));
|
|
350
|
+
return {
|
|
351
|
+
path: normalizedPath,
|
|
352
|
+
entries: entries2,
|
|
353
|
+
truncated: validEntries.length > maxEntries,
|
|
354
|
+
totalEntries: validEntries.length
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const metadataEntries = await this.mapWithConcurrency(
|
|
358
|
+
validEntries,
|
|
359
|
+
concurrency,
|
|
360
|
+
async (entry) => {
|
|
361
|
+
try {
|
|
362
|
+
const stat = await fs.stat(entry.normalizedPath);
|
|
363
|
+
return {
|
|
364
|
+
name: entry.entry.name,
|
|
365
|
+
path: entry.normalizedPath,
|
|
366
|
+
isDirectory: entry.entry.isDirectory(),
|
|
367
|
+
size: stat.size,
|
|
368
|
+
modified: stat.mtime
|
|
369
|
+
};
|
|
370
|
+
} catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
const entries = [];
|
|
376
|
+
let successfulStats = 0;
|
|
377
|
+
let cutoffIndex = -1;
|
|
378
|
+
for (let index = 0; index < metadataEntries.length; index += 1) {
|
|
379
|
+
const entry = metadataEntries[index];
|
|
380
|
+
if (!entry) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
successfulStats += 1;
|
|
384
|
+
if (entries.length < maxEntries) {
|
|
385
|
+
entries.push(entry);
|
|
386
|
+
}
|
|
387
|
+
if (successfulStats === maxEntries) {
|
|
388
|
+
cutoffIndex = index;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const remainingSuccessful = cutoffIndex >= 0 ? metadataEntries.slice(cutoffIndex + 1).filter((entry) => entry !== null).length : 0;
|
|
392
|
+
const totalEntries = successfulStats < maxEntries ? successfulStats : maxEntries + remainingSuccessful;
|
|
393
|
+
return {
|
|
394
|
+
path: normalizedPath,
|
|
395
|
+
entries,
|
|
396
|
+
truncated: totalEntries > maxEntries,
|
|
397
|
+
totalEntries
|
|
398
|
+
};
|
|
399
|
+
} catch (error) {
|
|
400
|
+
throw FileSystemError.listFailed(
|
|
401
|
+
normalizedPath,
|
|
402
|
+
error instanceof Error ? error.message : String(error)
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async mapWithConcurrency(items, limit, mapper) {
|
|
407
|
+
if (items.length === 0) {
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
const results = new Array(items.length);
|
|
411
|
+
let nextIndex = 0;
|
|
412
|
+
const workerCount = Math.min(Math.max(1, limit), items.length);
|
|
413
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
414
|
+
while (true) {
|
|
415
|
+
const current = nextIndex++;
|
|
416
|
+
if (current >= items.length) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const item = items[current];
|
|
420
|
+
if (item === void 0) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
results[current] = await mapper(item, current);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
await Promise.all(workers);
|
|
427
|
+
return results;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Create a directory
|
|
431
|
+
*/
|
|
432
|
+
async createDirectory(dirPath, options = {}) {
|
|
433
|
+
await this.ensureInitialized();
|
|
434
|
+
const validation = await this.pathValidator.validatePath(dirPath);
|
|
435
|
+
if (!validation.isValid || !validation.normalizedPath) {
|
|
436
|
+
throw FileSystemError.invalidPath(dirPath, validation.error || "Unknown error");
|
|
437
|
+
}
|
|
438
|
+
const normalizedPath = validation.normalizedPath;
|
|
439
|
+
const recursive = options.recursive ?? false;
|
|
440
|
+
try {
|
|
441
|
+
const firstCreated = await fs.mkdir(normalizedPath, { recursive });
|
|
442
|
+
const created = recursive ? typeof firstCreated === "string" : true;
|
|
443
|
+
return { path: normalizedPath, created };
|
|
444
|
+
} catch (error) {
|
|
445
|
+
const code = error.code;
|
|
446
|
+
if (code === "EEXIST") {
|
|
447
|
+
try {
|
|
448
|
+
const stat = await fs.stat(normalizedPath);
|
|
449
|
+
if (stat.isDirectory()) {
|
|
450
|
+
return { path: normalizedPath, created: false };
|
|
451
|
+
}
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
456
|
+
throw FileSystemError.permissionDenied(normalizedPath, "create directory");
|
|
457
|
+
}
|
|
458
|
+
throw FileSystemError.createDirFailed(
|
|
459
|
+
normalizedPath,
|
|
460
|
+
error instanceof Error ? error.message : String(error)
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Delete a file or directory
|
|
466
|
+
*/
|
|
467
|
+
async deletePath(targetPath, options = {}) {
|
|
468
|
+
await this.ensureInitialized();
|
|
469
|
+
const validation = await this.pathValidator.validatePath(targetPath);
|
|
470
|
+
if (!validation.isValid || !validation.normalizedPath) {
|
|
471
|
+
throw FileSystemError.invalidPath(targetPath, validation.error || "Unknown error");
|
|
472
|
+
}
|
|
473
|
+
const normalizedPath = validation.normalizedPath;
|
|
474
|
+
try {
|
|
475
|
+
await fs.rm(normalizedPath, { recursive: options.recursive ?? false, force: false });
|
|
476
|
+
return { path: normalizedPath, deleted: true };
|
|
477
|
+
} catch (error) {
|
|
478
|
+
const code = error.code;
|
|
479
|
+
if (code === "ENOENT") {
|
|
480
|
+
throw FileSystemError.fileNotFound(normalizedPath);
|
|
481
|
+
}
|
|
482
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
483
|
+
throw FileSystemError.permissionDenied(normalizedPath, "delete");
|
|
484
|
+
}
|
|
485
|
+
throw FileSystemError.deleteFailed(
|
|
486
|
+
normalizedPath,
|
|
487
|
+
error instanceof Error ? error.message : String(error)
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Rename or move a file or directory
|
|
493
|
+
*/
|
|
494
|
+
async renamePath(fromPath, toPath) {
|
|
495
|
+
await this.ensureInitialized();
|
|
496
|
+
const fromValidation = await this.pathValidator.validatePath(fromPath);
|
|
497
|
+
if (!fromValidation.isValid || !fromValidation.normalizedPath) {
|
|
498
|
+
throw FileSystemError.invalidPath(fromPath, fromValidation.error || "Unknown error");
|
|
499
|
+
}
|
|
500
|
+
const toValidation = await this.pathValidator.validatePath(toPath);
|
|
501
|
+
if (!toValidation.isValid || !toValidation.normalizedPath) {
|
|
502
|
+
throw FileSystemError.invalidPath(toPath, toValidation.error || "Unknown error");
|
|
503
|
+
}
|
|
504
|
+
const normalizedFrom = fromValidation.normalizedPath;
|
|
505
|
+
const normalizedTo = toValidation.normalizedPath;
|
|
506
|
+
if (normalizedFrom === normalizedTo) {
|
|
507
|
+
return { from: normalizedFrom, to: normalizedTo };
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
await fs.access(normalizedTo);
|
|
511
|
+
throw FileSystemError.renameFailed(
|
|
512
|
+
normalizedFrom,
|
|
513
|
+
`Target already exists: ${normalizedTo}`
|
|
514
|
+
);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
const code = error.code;
|
|
517
|
+
if (!code) {
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
if (code === "ENOENT") {
|
|
521
|
+
} else if (code === "EACCES" || code === "EPERM") {
|
|
522
|
+
throw FileSystemError.permissionDenied(normalizedTo, "rename");
|
|
523
|
+
} else {
|
|
524
|
+
throw FileSystemError.renameFailed(
|
|
525
|
+
normalizedFrom,
|
|
526
|
+
error instanceof Error ? error.message : String(error)
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
await fs.rename(normalizedFrom, normalizedTo);
|
|
532
|
+
return { from: normalizedFrom, to: normalizedTo };
|
|
533
|
+
} catch (error) {
|
|
534
|
+
const code = error.code;
|
|
535
|
+
if (code === "ENOENT") {
|
|
536
|
+
throw FileSystemError.fileNotFound(normalizedFrom);
|
|
537
|
+
}
|
|
538
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
539
|
+
throw FileSystemError.permissionDenied(normalizedFrom, "rename");
|
|
540
|
+
}
|
|
541
|
+
throw FileSystemError.renameFailed(
|
|
542
|
+
normalizedFrom,
|
|
543
|
+
error instanceof Error ? error.message : String(error)
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
232
547
|
/**
|
|
233
548
|
* Search for content in files (grep-like functionality)
|
|
234
549
|
*/
|
|
@@ -36,10 +36,12 @@ const createMockLogger = () => ({
|
|
|
36
36
|
(0, import_vitest.describe)("FileSystemService", () => {
|
|
37
37
|
let mockLogger;
|
|
38
38
|
let tempDir;
|
|
39
|
+
let backupDir;
|
|
39
40
|
(0, import_vitest.beforeEach)(async () => {
|
|
40
41
|
mockLogger = createMockLogger();
|
|
41
42
|
const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-fs-test-"));
|
|
42
43
|
tempDir = await fs.realpath(rawTempDir);
|
|
44
|
+
backupDir = path.join(tempDir, ".dexto", "backups");
|
|
43
45
|
import_vitest.vi.clearAllMocks();
|
|
44
46
|
});
|
|
45
47
|
(0, import_vitest.afterEach)(async () => {
|
|
@@ -59,6 +61,7 @@ const createMockLogger = () => ({
|
|
|
59
61
|
maxFileSize: 10 * 1024 * 1024,
|
|
60
62
|
workingDirectory: tempDir,
|
|
61
63
|
enableBackups: false,
|
|
64
|
+
backupPath: backupDir,
|
|
62
65
|
backupRetentionDays: 7
|
|
63
66
|
},
|
|
64
67
|
mockLogger
|
|
@@ -69,7 +72,6 @@ const createMockLogger = () => ({
|
|
|
69
72
|
const result = await fileSystemService.writeFile(testFile, "new content");
|
|
70
73
|
(0, import_vitest.expect)(result.success).toBe(true);
|
|
71
74
|
(0, import_vitest.expect)(result.backupPath).toBeUndefined();
|
|
72
|
-
const backupDir = path.join(tempDir, ".dexto-backups");
|
|
73
75
|
try {
|
|
74
76
|
const files = await fs.readdir(backupDir);
|
|
75
77
|
(0, import_vitest.expect)(files.length).toBe(0);
|
|
@@ -85,6 +87,7 @@ const createMockLogger = () => ({
|
|
|
85
87
|
maxFileSize: 10 * 1024 * 1024,
|
|
86
88
|
workingDirectory: tempDir,
|
|
87
89
|
enableBackups: true,
|
|
90
|
+
backupPath: backupDir,
|
|
88
91
|
backupRetentionDays: 7
|
|
89
92
|
},
|
|
90
93
|
mockLogger
|
|
@@ -95,7 +98,7 @@ const createMockLogger = () => ({
|
|
|
95
98
|
const result = await fileSystemService.writeFile(testFile, "new content");
|
|
96
99
|
(0, import_vitest.expect)(result.success).toBe(true);
|
|
97
100
|
(0, import_vitest.expect)(result.backupPath).toBeDefined();
|
|
98
|
-
(0, import_vitest.expect)(result.backupPath).toContain(
|
|
101
|
+
(0, import_vitest.expect)(result.backupPath).toContain(backupDir);
|
|
99
102
|
(0, import_vitest.expect)(result.backupPath).toContain("backup");
|
|
100
103
|
const backupContent = await fs.readFile(result.backupPath, "utf-8");
|
|
101
104
|
(0, import_vitest.expect)(backupContent).toBe("original content");
|
|
@@ -111,6 +114,7 @@ const createMockLogger = () => ({
|
|
|
111
114
|
maxFileSize: 10 * 1024 * 1024,
|
|
112
115
|
workingDirectory: tempDir,
|
|
113
116
|
enableBackups: true,
|
|
117
|
+
backupPath: backupDir,
|
|
114
118
|
backupRetentionDays: 7
|
|
115
119
|
},
|
|
116
120
|
mockLogger
|
|
@@ -133,6 +137,7 @@ const createMockLogger = () => ({
|
|
|
133
137
|
workingDirectory: tempDir,
|
|
134
138
|
enableBackups: false,
|
|
135
139
|
// Config says no backups
|
|
140
|
+
backupPath: backupDir,
|
|
136
141
|
backupRetentionDays: 7
|
|
137
142
|
},
|
|
138
143
|
mockLogger
|
|
@@ -157,6 +162,7 @@ const createMockLogger = () => ({
|
|
|
157
162
|
maxFileSize: 10 * 1024 * 1024,
|
|
158
163
|
workingDirectory: tempDir,
|
|
159
164
|
enableBackups: false,
|
|
165
|
+
backupPath: backupDir,
|
|
160
166
|
backupRetentionDays: 7
|
|
161
167
|
},
|
|
162
168
|
mockLogger
|
|
@@ -182,6 +188,7 @@ const createMockLogger = () => ({
|
|
|
182
188
|
maxFileSize: 10 * 1024 * 1024,
|
|
183
189
|
workingDirectory: tempDir,
|
|
184
190
|
enableBackups: true,
|
|
191
|
+
backupPath: backupDir,
|
|
185
192
|
backupRetentionDays: 7
|
|
186
193
|
},
|
|
187
194
|
mockLogger
|
|
@@ -210,6 +217,7 @@ const createMockLogger = () => ({
|
|
|
210
217
|
workingDirectory: tempDir,
|
|
211
218
|
enableBackups: false,
|
|
212
219
|
// Config says no backups
|
|
220
|
+
backupPath: backupDir,
|
|
213
221
|
backupRetentionDays: 7
|
|
214
222
|
},
|
|
215
223
|
mockLogger
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filesystem-service.test.d.ts","sourceRoot":"","sources":["../src/filesystem-service.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -13,10 +13,12 @@ const createMockLogger = () => ({
|
|
|
13
13
|
describe("FileSystemService", () => {
|
|
14
14
|
let mockLogger;
|
|
15
15
|
let tempDir;
|
|
16
|
+
let backupDir;
|
|
16
17
|
beforeEach(async () => {
|
|
17
18
|
mockLogger = createMockLogger();
|
|
18
19
|
const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-fs-test-"));
|
|
19
20
|
tempDir = await fs.realpath(rawTempDir);
|
|
21
|
+
backupDir = path.join(tempDir, ".dexto", "backups");
|
|
20
22
|
vi.clearAllMocks();
|
|
21
23
|
});
|
|
22
24
|
afterEach(async () => {
|
|
@@ -36,6 +38,7 @@ describe("FileSystemService", () => {
|
|
|
36
38
|
maxFileSize: 10 * 1024 * 1024,
|
|
37
39
|
workingDirectory: tempDir,
|
|
38
40
|
enableBackups: false,
|
|
41
|
+
backupPath: backupDir,
|
|
39
42
|
backupRetentionDays: 7
|
|
40
43
|
},
|
|
41
44
|
mockLogger
|
|
@@ -46,7 +49,6 @@ describe("FileSystemService", () => {
|
|
|
46
49
|
const result = await fileSystemService.writeFile(testFile, "new content");
|
|
47
50
|
expect(result.success).toBe(true);
|
|
48
51
|
expect(result.backupPath).toBeUndefined();
|
|
49
|
-
const backupDir = path.join(tempDir, ".dexto-backups");
|
|
50
52
|
try {
|
|
51
53
|
const files = await fs.readdir(backupDir);
|
|
52
54
|
expect(files.length).toBe(0);
|
|
@@ -62,6 +64,7 @@ describe("FileSystemService", () => {
|
|
|
62
64
|
maxFileSize: 10 * 1024 * 1024,
|
|
63
65
|
workingDirectory: tempDir,
|
|
64
66
|
enableBackups: true,
|
|
67
|
+
backupPath: backupDir,
|
|
65
68
|
backupRetentionDays: 7
|
|
66
69
|
},
|
|
67
70
|
mockLogger
|
|
@@ -72,7 +75,7 @@ describe("FileSystemService", () => {
|
|
|
72
75
|
const result = await fileSystemService.writeFile(testFile, "new content");
|
|
73
76
|
expect(result.success).toBe(true);
|
|
74
77
|
expect(result.backupPath).toBeDefined();
|
|
75
|
-
expect(result.backupPath).toContain(
|
|
78
|
+
expect(result.backupPath).toContain(backupDir);
|
|
76
79
|
expect(result.backupPath).toContain("backup");
|
|
77
80
|
const backupContent = await fs.readFile(result.backupPath, "utf-8");
|
|
78
81
|
expect(backupContent).toBe("original content");
|
|
@@ -88,6 +91,7 @@ describe("FileSystemService", () => {
|
|
|
88
91
|
maxFileSize: 10 * 1024 * 1024,
|
|
89
92
|
workingDirectory: tempDir,
|
|
90
93
|
enableBackups: true,
|
|
94
|
+
backupPath: backupDir,
|
|
91
95
|
backupRetentionDays: 7
|
|
92
96
|
},
|
|
93
97
|
mockLogger
|
|
@@ -110,6 +114,7 @@ describe("FileSystemService", () => {
|
|
|
110
114
|
workingDirectory: tempDir,
|
|
111
115
|
enableBackups: false,
|
|
112
116
|
// Config says no backups
|
|
117
|
+
backupPath: backupDir,
|
|
113
118
|
backupRetentionDays: 7
|
|
114
119
|
},
|
|
115
120
|
mockLogger
|
|
@@ -134,6 +139,7 @@ describe("FileSystemService", () => {
|
|
|
134
139
|
maxFileSize: 10 * 1024 * 1024,
|
|
135
140
|
workingDirectory: tempDir,
|
|
136
141
|
enableBackups: false,
|
|
142
|
+
backupPath: backupDir,
|
|
137
143
|
backupRetentionDays: 7
|
|
138
144
|
},
|
|
139
145
|
mockLogger
|
|
@@ -159,6 +165,7 @@ describe("FileSystemService", () => {
|
|
|
159
165
|
maxFileSize: 10 * 1024 * 1024,
|
|
160
166
|
workingDirectory: tempDir,
|
|
161
167
|
enableBackups: true,
|
|
168
|
+
backupPath: backupDir,
|
|
162
169
|
backupRetentionDays: 7
|
|
163
170
|
},
|
|
164
171
|
mockLogger
|
|
@@ -187,6 +194,7 @@ describe("FileSystemService", () => {
|
|
|
187
194
|
workingDirectory: tempDir,
|
|
188
195
|
enableBackups: false,
|
|
189
196
|
// Config says no backups
|
|
197
|
+
backupPath: backupDir,
|
|
190
198
|
backupRetentionDays: 7
|
|
191
199
|
},
|
|
192
200
|
mockLogger
|