@dexto/tools-filesystem 1.5.2 → 1.5.4
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.integration.test.cjs +36 -32
- package/dist/directory-approval.integration.test.js +36 -32
- package/dist/edit-file-tool.cjs +43 -19
- package/dist/edit-file-tool.js +43 -19
- package/dist/edit-file-tool.test.cjs +203 -0
- package/dist/edit-file-tool.test.d.cts +2 -0
- package/dist/edit-file-tool.test.d.ts +2 -0
- package/dist/edit-file-tool.test.js +180 -0
- package/dist/filesystem-service.cjs +24 -14
- package/dist/filesystem-service.d.cts +8 -3
- package/dist/filesystem-service.d.ts +8 -3
- package/dist/filesystem-service.js +24 -14
- package/dist/filesystem-service.test.cjs +233 -0
- package/dist/filesystem-service.test.d.cts +2 -0
- package/dist/filesystem-service.test.d.ts +2 -0
- package/dist/filesystem-service.test.js +210 -0
- package/dist/glob-files-tool.cjs +56 -3
- package/dist/glob-files-tool.d.cts +4 -3
- package/dist/glob-files-tool.d.ts +4 -3
- package/dist/glob-files-tool.js +46 -3
- package/dist/grep-content-tool.cjs +55 -3
- package/dist/grep-content-tool.d.cts +4 -3
- package/dist/grep-content-tool.d.ts +4 -3
- package/dist/grep-content-tool.js +45 -3
- package/dist/path-validator.cjs +29 -20
- package/dist/path-validator.d.cts +9 -2
- package/dist/path-validator.d.ts +9 -2
- package/dist/path-validator.js +29 -20
- package/dist/path-validator.test.cjs +54 -48
- package/dist/path-validator.test.js +54 -48
- package/dist/read-file-tool.cjs +2 -2
- package/dist/read-file-tool.js +2 -2
- package/dist/tool-provider.cjs +22 -7
- package/dist/tool-provider.d.cts +4 -1
- package/dist/tool-provider.d.ts +4 -1
- package/dist/tool-provider.js +22 -7
- package/dist/types.d.cts +6 -0
- package/dist/types.d.ts +6 -0
- package/dist/write-file-tool.cjs +41 -7
- package/dist/write-file-tool.js +46 -8
- package/dist/write-file-tool.test.cjs +217 -0
- package/dist/write-file-tool.test.d.cts +2 -0
- package/dist/write-file-tool.test.d.ts +2 -0
- package/dist/write-file-tool.test.js +194 -0
- package/package.json +2 -2
|
@@ -33,6 +33,13 @@ class FileSystemService {
|
|
|
33
33
|
getBackupDir() {
|
|
34
34
|
return this.config.backupPath || getDextoPath("backups");
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Get the effective working directory for file operations.
|
|
38
|
+
* Falls back to process.cwd() if not configured.
|
|
39
|
+
*/
|
|
40
|
+
getWorkingDirectory() {
|
|
41
|
+
return this.config.workingDirectory || process.cwd();
|
|
42
|
+
}
|
|
36
43
|
/**
|
|
37
44
|
* Initialize the service.
|
|
38
45
|
* Safe to call multiple times - subsequent calls return the same promise.
|
|
@@ -93,7 +100,7 @@ class FileSystemService {
|
|
|
93
100
|
* @param filePath The file path to check (can be relative or absolute)
|
|
94
101
|
* @returns true if the path is within config-allowed paths, false otherwise
|
|
95
102
|
*/
|
|
96
|
-
isPathWithinConfigAllowed(filePath) {
|
|
103
|
+
async isPathWithinConfigAllowed(filePath) {
|
|
97
104
|
return this.pathValidator.isPathWithinAllowed(filePath);
|
|
98
105
|
}
|
|
99
106
|
/**
|
|
@@ -101,7 +108,7 @@ class FileSystemService {
|
|
|
101
108
|
*/
|
|
102
109
|
async readFile(filePath, options = {}) {
|
|
103
110
|
await this.ensureInitialized();
|
|
104
|
-
const validation = this.pathValidator.validatePath(filePath);
|
|
111
|
+
const validation = await this.pathValidator.validatePath(filePath);
|
|
105
112
|
if (!validation.isValid || !validation.normalizedPath) {
|
|
106
113
|
throw FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
|
|
107
114
|
}
|
|
@@ -178,7 +185,7 @@ class FileSystemService {
|
|
|
178
185
|
});
|
|
179
186
|
const validFiles = [];
|
|
180
187
|
for (const file of files) {
|
|
181
|
-
const validation = this.pathValidator.validatePath(file);
|
|
188
|
+
const validation = await this.pathValidator.validatePath(file);
|
|
182
189
|
if (!validation.isValid || !validation.normalizedPath) {
|
|
183
190
|
this.logger.debug(`Skipping invalid path: ${file}`);
|
|
184
191
|
continue;
|
|
@@ -311,7 +318,7 @@ class FileSystemService {
|
|
|
311
318
|
*/
|
|
312
319
|
async writeFile(filePath, content, options = {}) {
|
|
313
320
|
await this.ensureInitialized();
|
|
314
|
-
const validation = this.pathValidator.validatePath(filePath);
|
|
321
|
+
const validation = await this.pathValidator.validatePath(filePath);
|
|
315
322
|
if (!validation.isValid || !validation.normalizedPath) {
|
|
316
323
|
throw FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
|
|
317
324
|
}
|
|
@@ -353,14 +360,14 @@ class FileSystemService {
|
|
|
353
360
|
*/
|
|
354
361
|
async editFile(filePath, operation, options = {}) {
|
|
355
362
|
await this.ensureInitialized();
|
|
356
|
-
const validation = this.pathValidator.validatePath(filePath);
|
|
363
|
+
const validation = await this.pathValidator.validatePath(filePath);
|
|
357
364
|
if (!validation.isValid || !validation.normalizedPath) {
|
|
358
365
|
throw FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
|
|
359
366
|
}
|
|
360
367
|
const normalizedPath = validation.normalizedPath;
|
|
361
368
|
const fileContent = await this.readFile(normalizedPath);
|
|
362
|
-
|
|
363
|
-
const occurrences = (
|
|
369
|
+
const originalContent = fileContent.content;
|
|
370
|
+
const occurrences = (originalContent.match(
|
|
364
371
|
new RegExp(operation.oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")
|
|
365
372
|
) || []).length;
|
|
366
373
|
if (occurrences === 0) {
|
|
@@ -374,21 +381,24 @@ class FileSystemService {
|
|
|
374
381
|
backupPath = await this.createBackup(normalizedPath);
|
|
375
382
|
}
|
|
376
383
|
try {
|
|
384
|
+
let newContent;
|
|
377
385
|
if (operation.replaceAll) {
|
|
378
|
-
|
|
386
|
+
newContent = originalContent.replace(
|
|
379
387
|
new RegExp(operation.oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
|
|
380
388
|
operation.newString
|
|
381
389
|
);
|
|
382
390
|
} else {
|
|
383
|
-
|
|
391
|
+
newContent = originalContent.replace(operation.oldString, operation.newString);
|
|
384
392
|
}
|
|
385
|
-
await fs.writeFile(normalizedPath,
|
|
393
|
+
await fs.writeFile(normalizedPath, newContent, options.encoding || DEFAULT_ENCODING);
|
|
386
394
|
this.logger.debug(`File edited: ${normalizedPath} (${occurrences} replacements)`);
|
|
387
395
|
return {
|
|
388
396
|
success: true,
|
|
389
397
|
path: normalizedPath,
|
|
390
398
|
changesCount: occurrences,
|
|
391
|
-
backupPath
|
|
399
|
+
backupPath,
|
|
400
|
+
originalContent,
|
|
401
|
+
newContent
|
|
392
402
|
};
|
|
393
403
|
} catch (error) {
|
|
394
404
|
throw FileSystemError.editFailed(
|
|
@@ -480,10 +490,10 @@ class FileSystemService {
|
|
|
480
490
|
return { ...this.config };
|
|
481
491
|
}
|
|
482
492
|
/**
|
|
483
|
-
* Check if a path is allowed
|
|
493
|
+
* Check if a path is allowed (async for non-blocking symlink resolution)
|
|
484
494
|
*/
|
|
485
|
-
isPathAllowed(filePath) {
|
|
486
|
-
const validation = this.pathValidator.validatePath(filePath);
|
|
495
|
+
async isPathAllowed(filePath) {
|
|
496
|
+
const validation = await this.pathValidator.validatePath(filePath);
|
|
487
497
|
return validation.isValid;
|
|
488
498
|
}
|
|
489
499
|
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
var import_vitest = require("vitest");
|
|
25
|
+
var path = __toESM(require("node:path"), 1);
|
|
26
|
+
var fs = __toESM(require("node:fs/promises"), 1);
|
|
27
|
+
var os = __toESM(require("node:os"), 1);
|
|
28
|
+
var import_filesystem_service = require("./filesystem-service.js");
|
|
29
|
+
const createMockLogger = () => ({
|
|
30
|
+
debug: import_vitest.vi.fn(),
|
|
31
|
+
info: import_vitest.vi.fn(),
|
|
32
|
+
warn: import_vitest.vi.fn(),
|
|
33
|
+
error: import_vitest.vi.fn(),
|
|
34
|
+
createChild: import_vitest.vi.fn().mockReturnThis()
|
|
35
|
+
});
|
|
36
|
+
(0, import_vitest.describe)("FileSystemService", () => {
|
|
37
|
+
let mockLogger;
|
|
38
|
+
let tempDir;
|
|
39
|
+
(0, import_vitest.beforeEach)(async () => {
|
|
40
|
+
mockLogger = createMockLogger();
|
|
41
|
+
const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-fs-test-"));
|
|
42
|
+
tempDir = await fs.realpath(rawTempDir);
|
|
43
|
+
import_vitest.vi.clearAllMocks();
|
|
44
|
+
});
|
|
45
|
+
(0, import_vitest.afterEach)(async () => {
|
|
46
|
+
try {
|
|
47
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
(0, import_vitest.describe)("Backup Behavior", () => {
|
|
52
|
+
(0, import_vitest.describe)("writeFile", () => {
|
|
53
|
+
(0, import_vitest.it)("should NOT create backup when enableBackups is false (default)", async () => {
|
|
54
|
+
const fileSystemService = new import_filesystem_service.FileSystemService(
|
|
55
|
+
{
|
|
56
|
+
allowedPaths: [tempDir],
|
|
57
|
+
blockedPaths: [],
|
|
58
|
+
blockedExtensions: [],
|
|
59
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
60
|
+
workingDirectory: tempDir,
|
|
61
|
+
enableBackups: false,
|
|
62
|
+
backupRetentionDays: 7
|
|
63
|
+
},
|
|
64
|
+
mockLogger
|
|
65
|
+
);
|
|
66
|
+
await fileSystemService.initialize();
|
|
67
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
68
|
+
await fs.writeFile(testFile, "original content");
|
|
69
|
+
const result = await fileSystemService.writeFile(testFile, "new content");
|
|
70
|
+
(0, import_vitest.expect)(result.success).toBe(true);
|
|
71
|
+
(0, import_vitest.expect)(result.backupPath).toBeUndefined();
|
|
72
|
+
const backupDir = path.join(tempDir, ".dexto-backups");
|
|
73
|
+
try {
|
|
74
|
+
const files = await fs.readdir(backupDir);
|
|
75
|
+
(0, import_vitest.expect)(files.length).toBe(0);
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
(0, import_vitest.it)("should create backup when enableBackups is true", async () => {
|
|
80
|
+
const fileSystemService = new import_filesystem_service.FileSystemService(
|
|
81
|
+
{
|
|
82
|
+
allowedPaths: [tempDir],
|
|
83
|
+
blockedPaths: [],
|
|
84
|
+
blockedExtensions: [],
|
|
85
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
86
|
+
workingDirectory: tempDir,
|
|
87
|
+
enableBackups: true,
|
|
88
|
+
backupRetentionDays: 7
|
|
89
|
+
},
|
|
90
|
+
mockLogger
|
|
91
|
+
);
|
|
92
|
+
await fileSystemService.initialize();
|
|
93
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
94
|
+
await fs.writeFile(testFile, "original content");
|
|
95
|
+
const result = await fileSystemService.writeFile(testFile, "new content");
|
|
96
|
+
(0, import_vitest.expect)(result.success).toBe(true);
|
|
97
|
+
(0, import_vitest.expect)(result.backupPath).toBeDefined();
|
|
98
|
+
(0, import_vitest.expect)(result.backupPath).toContain(".dexto");
|
|
99
|
+
(0, import_vitest.expect)(result.backupPath).toContain("backup");
|
|
100
|
+
const backupContent = await fs.readFile(result.backupPath, "utf-8");
|
|
101
|
+
(0, import_vitest.expect)(backupContent).toBe("original content");
|
|
102
|
+
const newContent = await fs.readFile(testFile, "utf-8");
|
|
103
|
+
(0, import_vitest.expect)(newContent).toBe("new content");
|
|
104
|
+
});
|
|
105
|
+
(0, import_vitest.it)("should NOT create backup for new files even when enableBackups is true", async () => {
|
|
106
|
+
const fileSystemService = new import_filesystem_service.FileSystemService(
|
|
107
|
+
{
|
|
108
|
+
allowedPaths: [tempDir],
|
|
109
|
+
blockedPaths: [],
|
|
110
|
+
blockedExtensions: [],
|
|
111
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
112
|
+
workingDirectory: tempDir,
|
|
113
|
+
enableBackups: true,
|
|
114
|
+
backupRetentionDays: 7
|
|
115
|
+
},
|
|
116
|
+
mockLogger
|
|
117
|
+
);
|
|
118
|
+
await fileSystemService.initialize();
|
|
119
|
+
const testFile = path.join(tempDir, "new-file.txt");
|
|
120
|
+
const result = await fileSystemService.writeFile(testFile, "content", {
|
|
121
|
+
createDirs: true
|
|
122
|
+
});
|
|
123
|
+
(0, import_vitest.expect)(result.success).toBe(true);
|
|
124
|
+
(0, import_vitest.expect)(result.backupPath).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
(0, import_vitest.it)("should respect per-call backup option over config", async () => {
|
|
127
|
+
const fileSystemService = new import_filesystem_service.FileSystemService(
|
|
128
|
+
{
|
|
129
|
+
allowedPaths: [tempDir],
|
|
130
|
+
blockedPaths: [],
|
|
131
|
+
blockedExtensions: [],
|
|
132
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
133
|
+
workingDirectory: tempDir,
|
|
134
|
+
enableBackups: false,
|
|
135
|
+
// Config says no backups
|
|
136
|
+
backupRetentionDays: 7
|
|
137
|
+
},
|
|
138
|
+
mockLogger
|
|
139
|
+
);
|
|
140
|
+
await fileSystemService.initialize();
|
|
141
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
142
|
+
await fs.writeFile(testFile, "original content");
|
|
143
|
+
const result = await fileSystemService.writeFile(testFile, "new content", {
|
|
144
|
+
backup: true
|
|
145
|
+
});
|
|
146
|
+
(0, import_vitest.expect)(result.success).toBe(true);
|
|
147
|
+
(0, import_vitest.expect)(result.backupPath).toBeDefined();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
(0, import_vitest.describe)("editFile", () => {
|
|
151
|
+
(0, import_vitest.it)("should NOT create backup when enableBackups is false (default)", async () => {
|
|
152
|
+
const fileSystemService = new import_filesystem_service.FileSystemService(
|
|
153
|
+
{
|
|
154
|
+
allowedPaths: [tempDir],
|
|
155
|
+
blockedPaths: [],
|
|
156
|
+
blockedExtensions: [],
|
|
157
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
158
|
+
workingDirectory: tempDir,
|
|
159
|
+
enableBackups: false,
|
|
160
|
+
backupRetentionDays: 7
|
|
161
|
+
},
|
|
162
|
+
mockLogger
|
|
163
|
+
);
|
|
164
|
+
await fileSystemService.initialize();
|
|
165
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
166
|
+
await fs.writeFile(testFile, "hello world");
|
|
167
|
+
const result = await fileSystemService.editFile(testFile, {
|
|
168
|
+
oldString: "world",
|
|
169
|
+
newString: "universe"
|
|
170
|
+
});
|
|
171
|
+
(0, import_vitest.expect)(result.success).toBe(true);
|
|
172
|
+
(0, import_vitest.expect)(result.backupPath).toBeUndefined();
|
|
173
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
174
|
+
(0, import_vitest.expect)(content).toBe("hello universe");
|
|
175
|
+
});
|
|
176
|
+
(0, import_vitest.it)("should create backup when enableBackups is true", async () => {
|
|
177
|
+
const fileSystemService = new import_filesystem_service.FileSystemService(
|
|
178
|
+
{
|
|
179
|
+
allowedPaths: [tempDir],
|
|
180
|
+
blockedPaths: [],
|
|
181
|
+
blockedExtensions: [],
|
|
182
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
183
|
+
workingDirectory: tempDir,
|
|
184
|
+
enableBackups: true,
|
|
185
|
+
backupRetentionDays: 7
|
|
186
|
+
},
|
|
187
|
+
mockLogger
|
|
188
|
+
);
|
|
189
|
+
await fileSystemService.initialize();
|
|
190
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
191
|
+
await fs.writeFile(testFile, "hello world");
|
|
192
|
+
const result = await fileSystemService.editFile(testFile, {
|
|
193
|
+
oldString: "world",
|
|
194
|
+
newString: "universe"
|
|
195
|
+
});
|
|
196
|
+
(0, import_vitest.expect)(result.success).toBe(true);
|
|
197
|
+
(0, import_vitest.expect)(result.backupPath).toBeDefined();
|
|
198
|
+
const backupContent = await fs.readFile(result.backupPath, "utf-8");
|
|
199
|
+
(0, import_vitest.expect)(backupContent).toBe("hello world");
|
|
200
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
201
|
+
(0, import_vitest.expect)(content).toBe("hello universe");
|
|
202
|
+
});
|
|
203
|
+
(0, import_vitest.it)("should respect per-call backup option over config", async () => {
|
|
204
|
+
const fileSystemService = new import_filesystem_service.FileSystemService(
|
|
205
|
+
{
|
|
206
|
+
allowedPaths: [tempDir],
|
|
207
|
+
blockedPaths: [],
|
|
208
|
+
blockedExtensions: [],
|
|
209
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
210
|
+
workingDirectory: tempDir,
|
|
211
|
+
enableBackups: false,
|
|
212
|
+
// Config says no backups
|
|
213
|
+
backupRetentionDays: 7
|
|
214
|
+
},
|
|
215
|
+
mockLogger
|
|
216
|
+
);
|
|
217
|
+
await fileSystemService.initialize();
|
|
218
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
219
|
+
await fs.writeFile(testFile, "hello world");
|
|
220
|
+
const result = await fileSystemService.editFile(
|
|
221
|
+
testFile,
|
|
222
|
+
{
|
|
223
|
+
oldString: "world",
|
|
224
|
+
newString: "universe"
|
|
225
|
+
},
|
|
226
|
+
{ backup: true }
|
|
227
|
+
);
|
|
228
|
+
(0, import_vitest.expect)(result.success).toBe(true);
|
|
229
|
+
(0, import_vitest.expect)(result.backupPath).toBeDefined();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { FileSystemService } from "./filesystem-service.js";
|
|
6
|
+
const createMockLogger = () => ({
|
|
7
|
+
debug: vi.fn(),
|
|
8
|
+
info: vi.fn(),
|
|
9
|
+
warn: vi.fn(),
|
|
10
|
+
error: vi.fn(),
|
|
11
|
+
createChild: vi.fn().mockReturnThis()
|
|
12
|
+
});
|
|
13
|
+
describe("FileSystemService", () => {
|
|
14
|
+
let mockLogger;
|
|
15
|
+
let tempDir;
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
mockLogger = createMockLogger();
|
|
18
|
+
const rawTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dexto-fs-test-"));
|
|
19
|
+
tempDir = await fs.realpath(rawTempDir);
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
try {
|
|
24
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
describe("Backup Behavior", () => {
|
|
29
|
+
describe("writeFile", () => {
|
|
30
|
+
it("should NOT create backup when enableBackups is false (default)", async () => {
|
|
31
|
+
const fileSystemService = new FileSystemService(
|
|
32
|
+
{
|
|
33
|
+
allowedPaths: [tempDir],
|
|
34
|
+
blockedPaths: [],
|
|
35
|
+
blockedExtensions: [],
|
|
36
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
37
|
+
workingDirectory: tempDir,
|
|
38
|
+
enableBackups: false,
|
|
39
|
+
backupRetentionDays: 7
|
|
40
|
+
},
|
|
41
|
+
mockLogger
|
|
42
|
+
);
|
|
43
|
+
await fileSystemService.initialize();
|
|
44
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
45
|
+
await fs.writeFile(testFile, "original content");
|
|
46
|
+
const result = await fileSystemService.writeFile(testFile, "new content");
|
|
47
|
+
expect(result.success).toBe(true);
|
|
48
|
+
expect(result.backupPath).toBeUndefined();
|
|
49
|
+
const backupDir = path.join(tempDir, ".dexto-backups");
|
|
50
|
+
try {
|
|
51
|
+
const files = await fs.readdir(backupDir);
|
|
52
|
+
expect(files.length).toBe(0);
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
it("should create backup when enableBackups is true", async () => {
|
|
57
|
+
const fileSystemService = new FileSystemService(
|
|
58
|
+
{
|
|
59
|
+
allowedPaths: [tempDir],
|
|
60
|
+
blockedPaths: [],
|
|
61
|
+
blockedExtensions: [],
|
|
62
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
63
|
+
workingDirectory: tempDir,
|
|
64
|
+
enableBackups: true,
|
|
65
|
+
backupRetentionDays: 7
|
|
66
|
+
},
|
|
67
|
+
mockLogger
|
|
68
|
+
);
|
|
69
|
+
await fileSystemService.initialize();
|
|
70
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
71
|
+
await fs.writeFile(testFile, "original content");
|
|
72
|
+
const result = await fileSystemService.writeFile(testFile, "new content");
|
|
73
|
+
expect(result.success).toBe(true);
|
|
74
|
+
expect(result.backupPath).toBeDefined();
|
|
75
|
+
expect(result.backupPath).toContain(".dexto");
|
|
76
|
+
expect(result.backupPath).toContain("backup");
|
|
77
|
+
const backupContent = await fs.readFile(result.backupPath, "utf-8");
|
|
78
|
+
expect(backupContent).toBe("original content");
|
|
79
|
+
const newContent = await fs.readFile(testFile, "utf-8");
|
|
80
|
+
expect(newContent).toBe("new content");
|
|
81
|
+
});
|
|
82
|
+
it("should NOT create backup for new files even when enableBackups is true", async () => {
|
|
83
|
+
const fileSystemService = new FileSystemService(
|
|
84
|
+
{
|
|
85
|
+
allowedPaths: [tempDir],
|
|
86
|
+
blockedPaths: [],
|
|
87
|
+
blockedExtensions: [],
|
|
88
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
89
|
+
workingDirectory: tempDir,
|
|
90
|
+
enableBackups: true,
|
|
91
|
+
backupRetentionDays: 7
|
|
92
|
+
},
|
|
93
|
+
mockLogger
|
|
94
|
+
);
|
|
95
|
+
await fileSystemService.initialize();
|
|
96
|
+
const testFile = path.join(tempDir, "new-file.txt");
|
|
97
|
+
const result = await fileSystemService.writeFile(testFile, "content", {
|
|
98
|
+
createDirs: true
|
|
99
|
+
});
|
|
100
|
+
expect(result.success).toBe(true);
|
|
101
|
+
expect(result.backupPath).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
it("should respect per-call backup option over config", async () => {
|
|
104
|
+
const fileSystemService = new FileSystemService(
|
|
105
|
+
{
|
|
106
|
+
allowedPaths: [tempDir],
|
|
107
|
+
blockedPaths: [],
|
|
108
|
+
blockedExtensions: [],
|
|
109
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
110
|
+
workingDirectory: tempDir,
|
|
111
|
+
enableBackups: false,
|
|
112
|
+
// Config says no backups
|
|
113
|
+
backupRetentionDays: 7
|
|
114
|
+
},
|
|
115
|
+
mockLogger
|
|
116
|
+
);
|
|
117
|
+
await fileSystemService.initialize();
|
|
118
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
119
|
+
await fs.writeFile(testFile, "original content");
|
|
120
|
+
const result = await fileSystemService.writeFile(testFile, "new content", {
|
|
121
|
+
backup: true
|
|
122
|
+
});
|
|
123
|
+
expect(result.success).toBe(true);
|
|
124
|
+
expect(result.backupPath).toBeDefined();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
describe("editFile", () => {
|
|
128
|
+
it("should NOT create backup when enableBackups is false (default)", async () => {
|
|
129
|
+
const fileSystemService = new FileSystemService(
|
|
130
|
+
{
|
|
131
|
+
allowedPaths: [tempDir],
|
|
132
|
+
blockedPaths: [],
|
|
133
|
+
blockedExtensions: [],
|
|
134
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
135
|
+
workingDirectory: tempDir,
|
|
136
|
+
enableBackups: false,
|
|
137
|
+
backupRetentionDays: 7
|
|
138
|
+
},
|
|
139
|
+
mockLogger
|
|
140
|
+
);
|
|
141
|
+
await fileSystemService.initialize();
|
|
142
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
143
|
+
await fs.writeFile(testFile, "hello world");
|
|
144
|
+
const result = await fileSystemService.editFile(testFile, {
|
|
145
|
+
oldString: "world",
|
|
146
|
+
newString: "universe"
|
|
147
|
+
});
|
|
148
|
+
expect(result.success).toBe(true);
|
|
149
|
+
expect(result.backupPath).toBeUndefined();
|
|
150
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
151
|
+
expect(content).toBe("hello universe");
|
|
152
|
+
});
|
|
153
|
+
it("should create backup when enableBackups is true", async () => {
|
|
154
|
+
const fileSystemService = new FileSystemService(
|
|
155
|
+
{
|
|
156
|
+
allowedPaths: [tempDir],
|
|
157
|
+
blockedPaths: [],
|
|
158
|
+
blockedExtensions: [],
|
|
159
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
160
|
+
workingDirectory: tempDir,
|
|
161
|
+
enableBackups: true,
|
|
162
|
+
backupRetentionDays: 7
|
|
163
|
+
},
|
|
164
|
+
mockLogger
|
|
165
|
+
);
|
|
166
|
+
await fileSystemService.initialize();
|
|
167
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
168
|
+
await fs.writeFile(testFile, "hello world");
|
|
169
|
+
const result = await fileSystemService.editFile(testFile, {
|
|
170
|
+
oldString: "world",
|
|
171
|
+
newString: "universe"
|
|
172
|
+
});
|
|
173
|
+
expect(result.success).toBe(true);
|
|
174
|
+
expect(result.backupPath).toBeDefined();
|
|
175
|
+
const backupContent = await fs.readFile(result.backupPath, "utf-8");
|
|
176
|
+
expect(backupContent).toBe("hello world");
|
|
177
|
+
const content = await fs.readFile(testFile, "utf-8");
|
|
178
|
+
expect(content).toBe("hello universe");
|
|
179
|
+
});
|
|
180
|
+
it("should respect per-call backup option over config", async () => {
|
|
181
|
+
const fileSystemService = new FileSystemService(
|
|
182
|
+
{
|
|
183
|
+
allowedPaths: [tempDir],
|
|
184
|
+
blockedPaths: [],
|
|
185
|
+
blockedExtensions: [],
|
|
186
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
187
|
+
workingDirectory: tempDir,
|
|
188
|
+
enableBackups: false,
|
|
189
|
+
// Config says no backups
|
|
190
|
+
backupRetentionDays: 7
|
|
191
|
+
},
|
|
192
|
+
mockLogger
|
|
193
|
+
);
|
|
194
|
+
await fileSystemService.initialize();
|
|
195
|
+
const testFile = path.join(tempDir, "test.txt");
|
|
196
|
+
await fs.writeFile(testFile, "hello world");
|
|
197
|
+
const result = await fileSystemService.editFile(
|
|
198
|
+
testFile,
|
|
199
|
+
{
|
|
200
|
+
oldString: "world",
|
|
201
|
+
newString: "universe"
|
|
202
|
+
},
|
|
203
|
+
{ backup: true }
|
|
204
|
+
);
|
|
205
|
+
expect(result.success).toBe(true);
|
|
206
|
+
expect(result.backupPath).toBeDefined();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
package/dist/glob-files-tool.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,27 +17,78 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
var glob_files_tool_exports = {};
|
|
20
30
|
__export(glob_files_tool_exports, {
|
|
21
31
|
createGlobFilesTool: () => createGlobFilesTool
|
|
22
32
|
});
|
|
23
33
|
module.exports = __toCommonJS(glob_files_tool_exports);
|
|
34
|
+
var path = __toESM(require("node:path"), 1);
|
|
24
35
|
var import_zod = require("zod");
|
|
36
|
+
var import_core = require("@dexto/core");
|
|
25
37
|
const GlobFilesInputSchema = import_zod.z.object({
|
|
26
38
|
pattern: import_zod.z.string().describe('Glob pattern to match files (e.g., "**/*.ts", "src/**/*.js")'),
|
|
27
39
|
path: import_zod.z.string().optional().describe("Base directory to search from (defaults to working directory)"),
|
|
28
40
|
max_results: import_zod.z.number().int().positive().optional().default(1e3).describe("Maximum number of results to return (default: 1000)")
|
|
29
41
|
}).strict();
|
|
30
|
-
function createGlobFilesTool(
|
|
42
|
+
function createGlobFilesTool(options) {
|
|
43
|
+
const { fileSystemService, directoryApproval } = options;
|
|
44
|
+
let pendingApprovalSearchDir;
|
|
31
45
|
return {
|
|
32
46
|
id: "glob_files",
|
|
33
47
|
description: "Find files matching a glob pattern. Supports standard glob syntax like **/*.js for recursive matches, *.ts for files in current directory, and src/**/*.tsx for nested paths. Returns array of file paths with metadata (size, modified date). Results are limited to allowed paths only.",
|
|
34
48
|
inputSchema: GlobFilesInputSchema,
|
|
49
|
+
/**
|
|
50
|
+
* Check if this glob operation needs directory access approval.
|
|
51
|
+
* Returns custom approval request if the search directory is outside allowed paths.
|
|
52
|
+
*/
|
|
53
|
+
getApprovalOverride: async (args) => {
|
|
54
|
+
const { path: searchPath } = args;
|
|
55
|
+
const baseDir = fileSystemService.getWorkingDirectory();
|
|
56
|
+
const searchDir = path.resolve(baseDir, searchPath || ".");
|
|
57
|
+
const isAllowed = await fileSystemService.isPathWithinConfigAllowed(searchDir);
|
|
58
|
+
if (isAllowed) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (directoryApproval?.isSessionApproved(searchDir)) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
pendingApprovalSearchDir = searchDir;
|
|
65
|
+
return {
|
|
66
|
+
type: import_core.ApprovalType.DIRECTORY_ACCESS,
|
|
67
|
+
metadata: {
|
|
68
|
+
path: searchDir,
|
|
69
|
+
parentDir: searchDir,
|
|
70
|
+
operation: "search",
|
|
71
|
+
toolName: "glob_files"
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
/**
|
|
76
|
+
* Handle approved directory access - remember the directory for session
|
|
77
|
+
*/
|
|
78
|
+
onApprovalGranted: (response) => {
|
|
79
|
+
if (!directoryApproval || !pendingApprovalSearchDir) return;
|
|
80
|
+
const data = response.data;
|
|
81
|
+
const rememberDirectory = data?.rememberDirectory ?? false;
|
|
82
|
+
directoryApproval.addApproved(
|
|
83
|
+
pendingApprovalSearchDir,
|
|
84
|
+
rememberDirectory ? "session" : "once"
|
|
85
|
+
);
|
|
86
|
+
pendingApprovalSearchDir = void 0;
|
|
87
|
+
},
|
|
35
88
|
execute: async (input, _context) => {
|
|
36
|
-
const { pattern, path, max_results } = input;
|
|
89
|
+
const { pattern, path: path2, max_results } = input;
|
|
37
90
|
const result = await fileSystemService.globFiles(pattern, {
|
|
38
|
-
cwd:
|
|
91
|
+
cwd: path2,
|
|
39
92
|
maxResults: max_results,
|
|
40
93
|
includeMetadata: true
|
|
41
94
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InternalTool } from '@dexto/core';
|
|
2
|
-
import {
|
|
2
|
+
import { FileToolOptions } from './file-tool-types.cjs';
|
|
3
|
+
import './filesystem-service.cjs';
|
|
3
4
|
import './types.cjs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -9,8 +10,8 @@ import './types.cjs';
|
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Create the glob_files internal tool
|
|
13
|
+
* Create the glob_files internal tool with directory approval support
|
|
13
14
|
*/
|
|
14
|
-
declare function createGlobFilesTool(
|
|
15
|
+
declare function createGlobFilesTool(options: FileToolOptions): InternalTool;
|
|
15
16
|
|
|
16
17
|
export { createGlobFilesTool };
|