@dexto/tools-filesystem 1.5.2 → 1.5.3
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 +17 -14
- package/dist/filesystem-service.d.cts +3 -3
- package/dist/filesystem-service.d.ts +3 -3
- package/dist/filesystem-service.js +17 -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/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
|
@@ -93,7 +93,7 @@ class FileSystemService {
|
|
|
93
93
|
* @param filePath The file path to check (can be relative or absolute)
|
|
94
94
|
* @returns true if the path is within config-allowed paths, false otherwise
|
|
95
95
|
*/
|
|
96
|
-
isPathWithinConfigAllowed(filePath) {
|
|
96
|
+
async isPathWithinConfigAllowed(filePath) {
|
|
97
97
|
return this.pathValidator.isPathWithinAllowed(filePath);
|
|
98
98
|
}
|
|
99
99
|
/**
|
|
@@ -101,7 +101,7 @@ class FileSystemService {
|
|
|
101
101
|
*/
|
|
102
102
|
async readFile(filePath, options = {}) {
|
|
103
103
|
await this.ensureInitialized();
|
|
104
|
-
const validation = this.pathValidator.validatePath(filePath);
|
|
104
|
+
const validation = await this.pathValidator.validatePath(filePath);
|
|
105
105
|
if (!validation.isValid || !validation.normalizedPath) {
|
|
106
106
|
throw FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
|
|
107
107
|
}
|
|
@@ -178,7 +178,7 @@ class FileSystemService {
|
|
|
178
178
|
});
|
|
179
179
|
const validFiles = [];
|
|
180
180
|
for (const file of files) {
|
|
181
|
-
const validation = this.pathValidator.validatePath(file);
|
|
181
|
+
const validation = await this.pathValidator.validatePath(file);
|
|
182
182
|
if (!validation.isValid || !validation.normalizedPath) {
|
|
183
183
|
this.logger.debug(`Skipping invalid path: ${file}`);
|
|
184
184
|
continue;
|
|
@@ -311,7 +311,7 @@ class FileSystemService {
|
|
|
311
311
|
*/
|
|
312
312
|
async writeFile(filePath, content, options = {}) {
|
|
313
313
|
await this.ensureInitialized();
|
|
314
|
-
const validation = this.pathValidator.validatePath(filePath);
|
|
314
|
+
const validation = await this.pathValidator.validatePath(filePath);
|
|
315
315
|
if (!validation.isValid || !validation.normalizedPath) {
|
|
316
316
|
throw FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
|
|
317
317
|
}
|
|
@@ -353,14 +353,14 @@ class FileSystemService {
|
|
|
353
353
|
*/
|
|
354
354
|
async editFile(filePath, operation, options = {}) {
|
|
355
355
|
await this.ensureInitialized();
|
|
356
|
-
const validation = this.pathValidator.validatePath(filePath);
|
|
356
|
+
const validation = await this.pathValidator.validatePath(filePath);
|
|
357
357
|
if (!validation.isValid || !validation.normalizedPath) {
|
|
358
358
|
throw FileSystemError.invalidPath(filePath, validation.error || "Unknown error");
|
|
359
359
|
}
|
|
360
360
|
const normalizedPath = validation.normalizedPath;
|
|
361
361
|
const fileContent = await this.readFile(normalizedPath);
|
|
362
|
-
|
|
363
|
-
const occurrences = (
|
|
362
|
+
const originalContent = fileContent.content;
|
|
363
|
+
const occurrences = (originalContent.match(
|
|
364
364
|
new RegExp(operation.oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")
|
|
365
365
|
) || []).length;
|
|
366
366
|
if (occurrences === 0) {
|
|
@@ -374,21 +374,24 @@ class FileSystemService {
|
|
|
374
374
|
backupPath = await this.createBackup(normalizedPath);
|
|
375
375
|
}
|
|
376
376
|
try {
|
|
377
|
+
let newContent;
|
|
377
378
|
if (operation.replaceAll) {
|
|
378
|
-
|
|
379
|
+
newContent = originalContent.replace(
|
|
379
380
|
new RegExp(operation.oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
|
|
380
381
|
operation.newString
|
|
381
382
|
);
|
|
382
383
|
} else {
|
|
383
|
-
|
|
384
|
+
newContent = originalContent.replace(operation.oldString, operation.newString);
|
|
384
385
|
}
|
|
385
|
-
await fs.writeFile(normalizedPath,
|
|
386
|
+
await fs.writeFile(normalizedPath, newContent, options.encoding || DEFAULT_ENCODING);
|
|
386
387
|
this.logger.debug(`File edited: ${normalizedPath} (${occurrences} replacements)`);
|
|
387
388
|
return {
|
|
388
389
|
success: true,
|
|
389
390
|
path: normalizedPath,
|
|
390
391
|
changesCount: occurrences,
|
|
391
|
-
backupPath
|
|
392
|
+
backupPath,
|
|
393
|
+
originalContent,
|
|
394
|
+
newContent
|
|
392
395
|
};
|
|
393
396
|
} catch (error) {
|
|
394
397
|
throw FileSystemError.editFailed(
|
|
@@ -480,10 +483,10 @@ class FileSystemService {
|
|
|
480
483
|
return { ...this.config };
|
|
481
484
|
}
|
|
482
485
|
/**
|
|
483
|
-
* Check if a path is allowed
|
|
486
|
+
* Check if a path is allowed (async for non-blocking symlink resolution)
|
|
484
487
|
*/
|
|
485
|
-
isPathAllowed(filePath) {
|
|
486
|
-
const validation = this.pathValidator.validatePath(filePath);
|
|
488
|
+
async isPathAllowed(filePath) {
|
|
489
|
+
const validation = await this.pathValidator.validatePath(filePath);
|
|
487
490
|
return validation.isValid;
|
|
488
491
|
}
|
|
489
492
|
}
|
|
@@ -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/path-validator.cjs
CHANGED
|
@@ -32,7 +32,7 @@ __export(path_validator_exports, {
|
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(path_validator_exports);
|
|
34
34
|
var path = __toESM(require("node:path"), 1);
|
|
35
|
-
var
|
|
35
|
+
var fs = __toESM(require("node:fs/promises"), 1);
|
|
36
36
|
class PathValidator {
|
|
37
37
|
config;
|
|
38
38
|
normalizedAllowedPaths;
|
|
@@ -67,7 +67,7 @@ class PathValidator {
|
|
|
67
67
|
/**
|
|
68
68
|
* Validate a file path for security and policy compliance
|
|
69
69
|
*/
|
|
70
|
-
validatePath(filePath) {
|
|
70
|
+
async validatePath(filePath) {
|
|
71
71
|
if (!filePath || filePath.trim() === "") {
|
|
72
72
|
return {
|
|
73
73
|
isValid: false,
|
|
@@ -79,7 +79,7 @@ class PathValidator {
|
|
|
79
79
|
try {
|
|
80
80
|
normalizedPath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(workingDir, filePath);
|
|
81
81
|
try {
|
|
82
|
-
normalizedPath =
|
|
82
|
+
normalizedPath = await fs.realpath(normalizedPath);
|
|
83
83
|
} catch {
|
|
84
84
|
}
|
|
85
85
|
} catch (error) {
|
|
@@ -135,22 +135,10 @@ class PathValidator {
|
|
|
135
135
|
/**
|
|
136
136
|
* Check if path is within allowed paths (whitelist check)
|
|
137
137
|
* Also consults the directory approval checker if configured.
|
|
138
|
+
* Uses the sync version since the path is already normalized at this point.
|
|
138
139
|
*/
|
|
139
140
|
isPathAllowed(normalizedPath) {
|
|
140
|
-
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
const isInConfigPaths = this.normalizedAllowedPaths.some((allowedPath) => {
|
|
144
|
-
const relative = path.relative(allowedPath, normalizedPath);
|
|
145
|
-
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
146
|
-
});
|
|
147
|
-
if (isInConfigPaths) {
|
|
148
|
-
return true;
|
|
149
|
-
}
|
|
150
|
-
if (this.directoryApprovalChecker) {
|
|
151
|
-
return this.directoryApprovalChecker(normalizedPath);
|
|
152
|
-
}
|
|
153
|
-
return false;
|
|
141
|
+
return this.isPathAllowedSync(normalizedPath);
|
|
154
142
|
}
|
|
155
143
|
/**
|
|
156
144
|
* Check if path matches blocked patterns (blacklist check)
|
|
@@ -169,9 +157,30 @@ class PathValidator {
|
|
|
169
157
|
}
|
|
170
158
|
/**
|
|
171
159
|
* Quick check if a path is allowed (for internal use)
|
|
160
|
+
* Note: This assumes the path is already normalized/canonicalized
|
|
172
161
|
*/
|
|
173
162
|
isPathAllowedQuick(normalizedPath) {
|
|
174
|
-
return this.
|
|
163
|
+
return this.isPathAllowedSync(normalizedPath) && !this.isPathBlocked(normalizedPath);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Synchronous path allowed check (for already-normalized paths)
|
|
167
|
+
* This is used internally when we already have a canonicalized path
|
|
168
|
+
*/
|
|
169
|
+
isPathAllowedSync(normalizedPath) {
|
|
170
|
+
if (this.normalizedAllowedPaths.length === 0) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
const isInConfigPaths = this.normalizedAllowedPaths.some((allowedPath) => {
|
|
174
|
+
const relative = path.relative(allowedPath, normalizedPath);
|
|
175
|
+
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
176
|
+
});
|
|
177
|
+
if (isInConfigPaths) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
if (this.directoryApprovalChecker) {
|
|
181
|
+
return this.directoryApprovalChecker(normalizedPath);
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
175
184
|
}
|
|
176
185
|
/**
|
|
177
186
|
* Check if a file path is within the configured allowed paths (from config only).
|
|
@@ -183,7 +192,7 @@ class PathValidator {
|
|
|
183
192
|
* @param filePath The file path to check (can be relative or absolute)
|
|
184
193
|
* @returns true if the path is within config-allowed paths, false otherwise
|
|
185
194
|
*/
|
|
186
|
-
isPathWithinAllowed(filePath) {
|
|
195
|
+
async isPathWithinAllowed(filePath) {
|
|
187
196
|
if (!filePath || filePath.trim() === "") {
|
|
188
197
|
return false;
|
|
189
198
|
}
|
|
@@ -192,7 +201,7 @@ class PathValidator {
|
|
|
192
201
|
try {
|
|
193
202
|
normalizedPath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(workingDir, filePath);
|
|
194
203
|
try {
|
|
195
|
-
normalizedPath =
|
|
204
|
+
normalizedPath = await fs.realpath(normalizedPath);
|
|
196
205
|
} catch {
|
|
197
206
|
}
|
|
198
207
|
} catch {
|
|
@@ -43,7 +43,7 @@ declare class PathValidator {
|
|
|
43
43
|
/**
|
|
44
44
|
* Validate a file path for security and policy compliance
|
|
45
45
|
*/
|
|
46
|
-
validatePath(filePath: string): PathValidation
|
|
46
|
+
validatePath(filePath: string): Promise<PathValidation>;
|
|
47
47
|
/**
|
|
48
48
|
* Check if path contains traversal attempts
|
|
49
49
|
*/
|
|
@@ -51,6 +51,7 @@ declare class PathValidator {
|
|
|
51
51
|
/**
|
|
52
52
|
* Check if path is within allowed paths (whitelist check)
|
|
53
53
|
* Also consults the directory approval checker if configured.
|
|
54
|
+
* Uses the sync version since the path is already normalized at this point.
|
|
54
55
|
*/
|
|
55
56
|
private isPathAllowed;
|
|
56
57
|
/**
|
|
@@ -59,8 +60,14 @@ declare class PathValidator {
|
|
|
59
60
|
private isPathBlocked;
|
|
60
61
|
/**
|
|
61
62
|
* Quick check if a path is allowed (for internal use)
|
|
63
|
+
* Note: This assumes the path is already normalized/canonicalized
|
|
62
64
|
*/
|
|
63
65
|
isPathAllowedQuick(normalizedPath: string): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Synchronous path allowed check (for already-normalized paths)
|
|
68
|
+
* This is used internally when we already have a canonicalized path
|
|
69
|
+
*/
|
|
70
|
+
private isPathAllowedSync;
|
|
64
71
|
/**
|
|
65
72
|
* Check if a file path is within the configured allowed paths (from config only).
|
|
66
73
|
* This method does NOT consult ApprovalManager - it only checks the static config paths.
|
|
@@ -71,7 +78,7 @@ declare class PathValidator {
|
|
|
71
78
|
* @param filePath The file path to check (can be relative or absolute)
|
|
72
79
|
* @returns true if the path is within config-allowed paths, false otherwise
|
|
73
80
|
*/
|
|
74
|
-
isPathWithinAllowed(filePath: string): boolean
|
|
81
|
+
isPathWithinAllowed(filePath: string): Promise<boolean>;
|
|
75
82
|
/**
|
|
76
83
|
* Check if path is within config-allowed paths only (no approval checker).
|
|
77
84
|
* Used for prompting decisions.
|