@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.
Files changed (95) hide show
  1. package/dist/directory-approval.cjs +98 -0
  2. package/dist/directory-approval.d.ts +24 -0
  3. package/dist/directory-approval.d.ts.map +1 -0
  4. package/dist/directory-approval.integration.test.cjs +175 -390
  5. package/dist/directory-approval.integration.test.d.ts +14 -2
  6. package/dist/directory-approval.integration.test.d.ts.map +1 -0
  7. package/dist/directory-approval.integration.test.js +178 -390
  8. package/dist/directory-approval.js +63 -0
  9. package/dist/edit-file-tool.cjs +109 -120
  10. package/dist/edit-file-tool.d.ts +22 -9
  11. package/dist/edit-file-tool.d.ts.map +1 -0
  12. package/dist/edit-file-tool.js +116 -110
  13. package/dist/edit-file-tool.test.cjs +109 -29
  14. package/dist/edit-file-tool.test.d.ts +7 -2
  15. package/dist/edit-file-tool.test.d.ts.map +1 -0
  16. package/dist/edit-file-tool.test.js +109 -29
  17. package/dist/error-codes.cjs +4 -0
  18. package/dist/error-codes.d.ts +6 -3
  19. package/dist/error-codes.d.ts.map +1 -0
  20. package/dist/error-codes.js +4 -0
  21. package/dist/errors.cjs +48 -0
  22. package/dist/errors.d.ts +20 -7
  23. package/dist/errors.d.ts.map +1 -0
  24. package/dist/errors.js +48 -0
  25. package/dist/file-tool-types.d.ts +8 -40
  26. package/dist/file-tool-types.d.ts.map +1 -0
  27. package/dist/filesystem-service.cjs +325 -10
  28. package/dist/filesystem-service.d.ts +41 -12
  29. package/dist/filesystem-service.d.ts.map +1 -0
  30. package/dist/filesystem-service.js +326 -11
  31. package/dist/filesystem-service.test.cjs +10 -2
  32. package/dist/filesystem-service.test.d.ts +7 -2
  33. package/dist/filesystem-service.test.d.ts.map +1 -0
  34. package/dist/filesystem-service.test.js +10 -2
  35. package/dist/glob-files-tool.cjs +32 -46
  36. package/dist/glob-files-tool.d.ts +19 -9
  37. package/dist/glob-files-tool.d.ts.map +1 -0
  38. package/dist/glob-files-tool.js +33 -47
  39. package/dist/grep-content-tool.cjs +40 -45
  40. package/dist/grep-content-tool.d.ts +28 -9
  41. package/dist/grep-content-tool.d.ts.map +1 -0
  42. package/dist/grep-content-tool.js +41 -46
  43. package/dist/index.cjs +6 -3
  44. package/dist/index.d.cts +852 -14
  45. package/dist/index.d.ts +11 -5
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +4 -2
  48. package/dist/path-validator.cjs +28 -2
  49. package/dist/path-validator.d.ts +20 -9
  50. package/dist/path-validator.d.ts.map +1 -0
  51. package/dist/path-validator.js +28 -2
  52. package/dist/path-validator.test.d.ts +7 -2
  53. package/dist/path-validator.test.d.ts.map +1 -0
  54. package/dist/read-file-tool.cjs +26 -59
  55. package/dist/read-file-tool.d.ts +19 -9
  56. package/dist/read-file-tool.d.ts.map +1 -0
  57. package/dist/read-file-tool.js +27 -50
  58. package/dist/tool-factory-config.cjs +61 -0
  59. package/dist/{tool-provider.d.ts → tool-factory-config.d.ts} +13 -30
  60. package/dist/tool-factory-config.d.ts.map +1 -0
  61. package/dist/tool-factory-config.js +36 -0
  62. package/dist/tool-factory.cjs +123 -0
  63. package/dist/tool-factory.d.ts +4 -0
  64. package/dist/tool-factory.d.ts.map +1 -0
  65. package/dist/tool-factory.js +102 -0
  66. package/dist/types.d.ts +82 -18
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/write-file-tool.cjs +93 -99
  69. package/dist/write-file-tool.d.ts +22 -9
  70. package/dist/write-file-tool.d.ts.map +1 -0
  71. package/dist/write-file-tool.js +97 -91
  72. package/dist/write-file-tool.test.cjs +139 -33
  73. package/dist/write-file-tool.test.d.ts +7 -2
  74. package/dist/write-file-tool.test.d.ts.map +1 -0
  75. package/dist/write-file-tool.test.js +139 -33
  76. package/package.json +5 -4
  77. package/dist/directory-approval.integration.test.d.cts +0 -2
  78. package/dist/edit-file-tool.d.cts +0 -17
  79. package/dist/edit-file-tool.test.d.cts +0 -2
  80. package/dist/error-codes.d.cts +0 -32
  81. package/dist/errors.d.cts +0 -112
  82. package/dist/file-tool-types.d.cts +0 -46
  83. package/dist/filesystem-service.d.cts +0 -112
  84. package/dist/filesystem-service.test.d.cts +0 -2
  85. package/dist/glob-files-tool.d.cts +0 -17
  86. package/dist/grep-content-tool.d.cts +0 -17
  87. package/dist/path-validator.d.cts +0 -97
  88. package/dist/path-validator.test.d.cts +0 -2
  89. package/dist/read-file-tool.d.cts +0 -17
  90. package/dist/tool-provider.cjs +0 -123
  91. package/dist/tool-provider.d.cts +0 -77
  92. package/dist/tool-provider.js +0 -99
  93. package/dist/types.d.cts +0 -178
  94. package/dist/write-file-tool.d.cts +0 -17
  95. 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 provider schema.
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
- * Read a file with validation and size limits
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
- const normalizedPath = validation.normalizedPath;
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: selectedLines.join("\n"),
181
+ content: returnedContent,
158
182
  lines: selectedLines.length,
159
183
  encoding,
160
184
  truncated,
161
- size: Buffer.byteLength(content, encoding)
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(".dexto");
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
@@ -1,2 +1,7 @@
1
-
2
- export { }
1
+ /**
2
+ * FileSystemService Tests
3
+ *
4
+ * Tests for the core filesystem service including backup behavior.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=filesystem-service.test.d.ts.map
@@ -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(".dexto");
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