mrmd-server 0.2.6 → 0.2.7
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/package.json +1 -2
- package/src/services.js +10 -10
- package/src/sync-manager.js +3 -3
- package/src/vendor/config.js +295 -0
- package/src/vendor/services/asset-service.js +332 -0
- package/src/vendor/services/file-service.js +520 -0
- package/src/vendor/services/languagetool-preferences-service.js +341 -0
- package/src/vendor/services/languagetool-service.js +763 -0
- package/src/vendor/services/project-service.js +439 -0
- package/src/vendor/services/runtime-preferences-service.js +603 -0
- package/src/vendor/services/runtime-service.js +1075 -0
- package/src/vendor/services/settings-service.js +715 -0
- package/src/vendor/utils/index.js +8 -0
- package/src/vendor/utils/network.js +116 -0
- package/src/vendor/utils/platform.js +472 -0
- package/src/vendor/utils/python.js +309 -0
- package/src/vendor/utils/uv-installer.js +299 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileService - File operations with automatic refactoring
|
|
3
|
+
*
|
|
4
|
+
* Manages file operations (create, move, delete) within mrmd projects.
|
|
5
|
+
* Automatically updates internal links and asset paths when files move.
|
|
6
|
+
*
|
|
7
|
+
* Uses mrmd-project for FSML parsing and link/asset refactoring.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { FSML, Links, Assets } from 'mrmd-project';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import fsPromises from 'fs/promises';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { UNORDERED_FILES, ASSETS_DIR_NAME } from '../config.js';
|
|
15
|
+
|
|
16
|
+
const DOC_EXTENSIONS = ['.md', '.qmd'];
|
|
17
|
+
|
|
18
|
+
function isDocFilename(filename) {
|
|
19
|
+
if (!filename) return false;
|
|
20
|
+
const lower = filename.toLowerCase();
|
|
21
|
+
return DOC_EXTENSIONS.some(ext => lower.endsWith(ext));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function semanticLinkName(filePath) {
|
|
25
|
+
if (!filePath) return '';
|
|
26
|
+
return filePath
|
|
27
|
+
.split('/')
|
|
28
|
+
.pop()
|
|
29
|
+
.replace(/\.[^.]+$/, '')
|
|
30
|
+
.replace(/^\d+-/, '')
|
|
31
|
+
.toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function needsGlobalLinkRefactor(fromPath, toPath) {
|
|
35
|
+
return semanticLinkName(fromPath) !== semanticLinkName(toPath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class FileService {
|
|
39
|
+
/**
|
|
40
|
+
* @param {ProjectService} projectService - Reference to ProjectService for cache invalidation
|
|
41
|
+
*/
|
|
42
|
+
constructor(projectService) {
|
|
43
|
+
this.projectService = projectService;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Scan files in a directory
|
|
48
|
+
*
|
|
49
|
+
* @param {string} root - Directory to scan
|
|
50
|
+
* @param {object} options - Scan options
|
|
51
|
+
* @param {boolean} options.includeHidden - Include hidden (_) directories
|
|
52
|
+
* @param {string[]} options.extensions - File extensions to include
|
|
53
|
+
* @param {number} options.maxDepth - Maximum recursion depth
|
|
54
|
+
* @returns {Promise<string[]>} Sorted relative paths
|
|
55
|
+
*/
|
|
56
|
+
async scan(root, options = {}) {
|
|
57
|
+
const {
|
|
58
|
+
includeHidden = false,
|
|
59
|
+
extensions = DOC_EXTENSIONS,
|
|
60
|
+
maxDepth = 10,
|
|
61
|
+
} = options;
|
|
62
|
+
|
|
63
|
+
const files = [];
|
|
64
|
+
|
|
65
|
+
const walk = async (dir, depth) => {
|
|
66
|
+
if (depth > maxDepth) return;
|
|
67
|
+
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
71
|
+
} catch {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const fullPath = path.join(dir, entry.name);
|
|
77
|
+
const relativePath = path.relative(root, fullPath);
|
|
78
|
+
|
|
79
|
+
// Skip system files (.)
|
|
80
|
+
if (entry.name.startsWith('.')) continue;
|
|
81
|
+
|
|
82
|
+
// Skip hidden files (_) unless requested
|
|
83
|
+
if (!includeHidden && entry.name.startsWith('_')) continue;
|
|
84
|
+
|
|
85
|
+
// Skip node_modules
|
|
86
|
+
if (entry.name === 'node_modules') continue;
|
|
87
|
+
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
await walk(fullPath, depth + 1);
|
|
90
|
+
} else {
|
|
91
|
+
// Check extension
|
|
92
|
+
const hasMatchingExt = extensions.some(ext => entry.name.endsWith(ext));
|
|
93
|
+
if (hasMatchingExt) {
|
|
94
|
+
files.push(relativePath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
await walk(root, 0);
|
|
101
|
+
return FSML.sortPaths(files);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create a file
|
|
106
|
+
*
|
|
107
|
+
* @param {string} filePath - Absolute path to create
|
|
108
|
+
* @param {string} content - File content
|
|
109
|
+
*/
|
|
110
|
+
async createFile(filePath, content = '') {
|
|
111
|
+
await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
|
|
112
|
+
await fsPromises.writeFile(filePath, content);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a file within a project (handles FSML ordering)
|
|
117
|
+
*
|
|
118
|
+
* @param {string} projectRoot - Project root path
|
|
119
|
+
* @param {string} relativePath - Desired relative path
|
|
120
|
+
* @param {string} content - File content
|
|
121
|
+
* @returns {Promise<string>} Actual relative path (may have order prefix)
|
|
122
|
+
*/
|
|
123
|
+
// Files that should never have order prefixes (using config from ../config.js)
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if a filename should bypass FSML ordering
|
|
127
|
+
*/
|
|
128
|
+
static shouldBypassOrdering(filename) {
|
|
129
|
+
const lower = filename.toLowerCase();
|
|
130
|
+
return UNORDERED_FILES.has(lower) || lower.startsWith('_');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async createInProject(projectRoot, relativePath, content = '') {
|
|
134
|
+
// Get the directory
|
|
135
|
+
const dir = path.dirname(relativePath);
|
|
136
|
+
const dirPath = dir ? path.join(projectRoot, dir) : projectRoot;
|
|
137
|
+
|
|
138
|
+
let finalPath = relativePath;
|
|
139
|
+
const filename = path.basename(relativePath);
|
|
140
|
+
|
|
141
|
+
// Skip ordering for special files (README.md/README.qmd, LICENSE, etc.)
|
|
142
|
+
if (FileService.shouldBypassOrdering(filename)) {
|
|
143
|
+
const fullPath = path.join(projectRoot, finalPath);
|
|
144
|
+
await this.createFile(fullPath, content);
|
|
145
|
+
|
|
146
|
+
if (this.projectService) {
|
|
147
|
+
this.projectService.invalidate(projectRoot);
|
|
148
|
+
}
|
|
149
|
+
return finalPath;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check if directory exists and has ordered files
|
|
153
|
+
if (await this.dirExists(dirPath)) {
|
|
154
|
+
try {
|
|
155
|
+
const siblings = await fsPromises.readdir(dirPath);
|
|
156
|
+
const docSiblings = siblings.filter(isDocFilename);
|
|
157
|
+
|
|
158
|
+
// Find max order among siblings
|
|
159
|
+
let maxOrder = 0;
|
|
160
|
+
let hasOrderedFiles = false;
|
|
161
|
+
|
|
162
|
+
for (const sibling of docSiblings) {
|
|
163
|
+
const parsed = FSML.parsePath(sibling);
|
|
164
|
+
if (parsed.order !== null) {
|
|
165
|
+
hasOrderedFiles = true;
|
|
166
|
+
maxOrder = Math.max(maxOrder, parsed.order);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// If folder has ordered files and new file doesn't have order, add one
|
|
171
|
+
if (hasOrderedFiles) {
|
|
172
|
+
const parsed = FSML.parsePath(filename);
|
|
173
|
+
|
|
174
|
+
if (parsed.order === null) {
|
|
175
|
+
// Add next order prefix
|
|
176
|
+
const newOrder = maxOrder + 1;
|
|
177
|
+
const paddedOrder = String(newOrder).padStart(2, '0');
|
|
178
|
+
const newFilename = `${paddedOrder}-${filename}`;
|
|
179
|
+
finalPath = dir ? path.join(dir, newFilename) : newFilename;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Directory doesn't exist yet, that's fine
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const fullPath = path.join(projectRoot, finalPath);
|
|
188
|
+
await this.createFile(fullPath, content);
|
|
189
|
+
|
|
190
|
+
// Invalidate project cache
|
|
191
|
+
if (this.projectService) {
|
|
192
|
+
this.projectService.invalidate(projectRoot);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return finalPath;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Reorder a file/folder using FSML conventions (for drag-drop)
|
|
200
|
+
*
|
|
201
|
+
* Uses FSML.computeReorder() with sibling information to properly
|
|
202
|
+
* shift all numbered items and maintain FSML ordering.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} projectRoot - Project root path
|
|
205
|
+
* @param {string} sourcePath - Source relative path
|
|
206
|
+
* @param {string} targetPath - Target relative path (drop target)
|
|
207
|
+
* @param {'before' | 'after' | 'inside'} position - Drop position
|
|
208
|
+
* @returns {Promise<RefactorResult>}
|
|
209
|
+
*/
|
|
210
|
+
async reorder(projectRoot, sourcePath, targetPath, position) {
|
|
211
|
+
// 1. Scan all files ONCE for both reorder computation and link refactoring
|
|
212
|
+
const allFiles = await this.scan(projectRoot, { includeHidden: true });
|
|
213
|
+
|
|
214
|
+
// 2. Use FSML.computeReorder with siblings for proper shift computation
|
|
215
|
+
const { newPath, renames } = FSML.computeReorder(sourcePath, targetPath, position, allFiles);
|
|
216
|
+
|
|
217
|
+
console.log(`[FileService.reorder] ${sourcePath} -> ${newPath} (${position})`);
|
|
218
|
+
console.log(`[FileService.reorder] Renames needed:`, renames);
|
|
219
|
+
|
|
220
|
+
// 3. If no renames needed, nothing to do
|
|
221
|
+
if (renames.length === 0) {
|
|
222
|
+
console.log(`[FileService.reorder] No changes needed`);
|
|
223
|
+
return { movedFile: sourcePath, updatedFiles: [] };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const updatedFiles = [];
|
|
227
|
+
|
|
228
|
+
// 4. Execute renames in order, passing pre-scanned files to avoid redundant scans
|
|
229
|
+
for (const rename of renames) {
|
|
230
|
+
try {
|
|
231
|
+
const result = await this.move(projectRoot, rename.from, rename.to, { _cachedFiles: allFiles });
|
|
232
|
+
updatedFiles.push(...result.updatedFiles);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
console.error(`[FileService.reorder] Failed to rename ${rename.from} -> ${rename.to}:`, e.message);
|
|
235
|
+
// Continue with other renames - partial success is better than nothing
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 5. Invalidate project cache once at the end (not per-move)
|
|
240
|
+
if (this.projectService) {
|
|
241
|
+
this.projectService.invalidate(projectRoot);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { movedFile: newPath, updatedFiles: [...new Set(updatedFiles)] };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Move/rename a file or folder with automatic refactoring
|
|
249
|
+
*
|
|
250
|
+
* @param {string} projectRoot - Project root path
|
|
251
|
+
* @param {string} fromPath - Source relative path
|
|
252
|
+
* @param {string} toPath - Destination relative path
|
|
253
|
+
* @returns {Promise<RefactorResult>}
|
|
254
|
+
*/
|
|
255
|
+
/**
|
|
256
|
+
* Move/rename a file or folder with automatic refactoring
|
|
257
|
+
*
|
|
258
|
+
* @param {string} projectRoot - Project root path
|
|
259
|
+
* @param {string} fromPath - Source relative path
|
|
260
|
+
* @param {string} toPath - Destination relative path
|
|
261
|
+
* @param {object} [options] - Internal options
|
|
262
|
+
* @param {string[]} [options._cachedFiles] - Pre-scanned file list (avoids redundant scans in batch ops)
|
|
263
|
+
* @returns {Promise<RefactorResult>}
|
|
264
|
+
*/
|
|
265
|
+
async move(projectRoot, fromPath, toPath, options = {}) {
|
|
266
|
+
const fullFromPath = path.join(projectRoot, fromPath);
|
|
267
|
+
const fullToPath = path.join(projectRoot, toPath);
|
|
268
|
+
|
|
269
|
+
// Check if source exists
|
|
270
|
+
let stat;
|
|
271
|
+
try {
|
|
272
|
+
stat = await fsPromises.stat(fullFromPath);
|
|
273
|
+
} catch (e) {
|
|
274
|
+
// Source doesn't exist - might have been renamed already in a batch
|
|
275
|
+
console.log(`[FileService.move] Source doesn't exist (may have been renamed): ${fromPath}`);
|
|
276
|
+
return { movedFile: toPath, updatedFiles: [] };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check if source is a directory
|
|
280
|
+
const isDirectory = stat.isDirectory();
|
|
281
|
+
|
|
282
|
+
if (isDirectory) {
|
|
283
|
+
return this.moveDirectory(projectRoot, fromPath, toPath, options);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const updatedFiles = [];
|
|
287
|
+
|
|
288
|
+
const shouldRefactorLinks = needsGlobalLinkRefactor(fromPath, toPath);
|
|
289
|
+
if (shouldRefactorLinks) {
|
|
290
|
+
// Use pre-scanned files if available, otherwise scan
|
|
291
|
+
const files = options._cachedFiles || await this.scan(projectRoot, { includeHidden: true });
|
|
292
|
+
|
|
293
|
+
// For each file, check if it references the moved file
|
|
294
|
+
for (const file of files) {
|
|
295
|
+
if (file === fromPath) continue;
|
|
296
|
+
|
|
297
|
+
const fullPath = path.join(projectRoot, file);
|
|
298
|
+
let content;
|
|
299
|
+
try {
|
|
300
|
+
content = await fsPromises.readFile(fullPath, 'utf8');
|
|
301
|
+
} catch (e) {
|
|
302
|
+
console.warn(`[file] Could not read ${file} for link refactoring:`, e.message);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Update links using mrmd-project
|
|
307
|
+
const updatedContent = Links.refactor(content, [
|
|
308
|
+
{ from: fromPath, to: toPath },
|
|
309
|
+
], file);
|
|
310
|
+
|
|
311
|
+
if (updatedContent !== content) {
|
|
312
|
+
await fsPromises.writeFile(fullPath, updatedContent);
|
|
313
|
+
updatedFiles.push(file);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Read the file being moved
|
|
319
|
+
let movingContent;
|
|
320
|
+
try {
|
|
321
|
+
movingContent = await fsPromises.readFile(fullFromPath, 'utf8');
|
|
322
|
+
} catch (e) {
|
|
323
|
+
throw new Error(`Cannot read source file: ${e.message}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Update asset paths IN the moved file using mrmd-project
|
|
327
|
+
const updatedMovingContent = Assets.refactorPaths(
|
|
328
|
+
movingContent,
|
|
329
|
+
fromPath,
|
|
330
|
+
toPath,
|
|
331
|
+
ASSETS_DIR_NAME
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// Actually move the file
|
|
335
|
+
await fsPromises.mkdir(path.dirname(fullToPath), { recursive: true });
|
|
336
|
+
await fsPromises.writeFile(fullToPath, updatedMovingContent);
|
|
337
|
+
await fsPromises.unlink(fullFromPath);
|
|
338
|
+
|
|
339
|
+
// Clean up empty directories
|
|
340
|
+
await this.removeEmptyDirs(path.dirname(fullFromPath), projectRoot);
|
|
341
|
+
|
|
342
|
+
// Invalidate project cache (skip if called from batch operation like reorder)
|
|
343
|
+
if (!options._cachedFiles && this.projectService) {
|
|
344
|
+
this.projectService.invalidate(projectRoot);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return { movedFile: toPath, updatedFiles };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Move/rename a directory with automatic refactoring
|
|
352
|
+
*
|
|
353
|
+
* @param {string} projectRoot - Project root path
|
|
354
|
+
* @param {string} fromPath - Source relative path (folder)
|
|
355
|
+
* @param {string} toPath - Destination relative path (folder)
|
|
356
|
+
* @returns {Promise<RefactorResult>}
|
|
357
|
+
*/
|
|
358
|
+
/**
|
|
359
|
+
* Move/rename a directory with automatic refactoring
|
|
360
|
+
*
|
|
361
|
+
* @param {string} projectRoot - Project root path
|
|
362
|
+
* @param {string} fromPath - Source relative path (folder)
|
|
363
|
+
* @param {string} toPath - Destination relative path (folder)
|
|
364
|
+
* @param {object} [options] - Internal options
|
|
365
|
+
* @param {string[]} [options._cachedFiles] - Pre-scanned file list
|
|
366
|
+
* @returns {Promise<RefactorResult>}
|
|
367
|
+
*/
|
|
368
|
+
async moveDirectory(projectRoot, fromPath, toPath, options = {}) {
|
|
369
|
+
const fullFromPath = path.join(projectRoot, fromPath);
|
|
370
|
+
const fullToPath = path.join(projectRoot, toPath);
|
|
371
|
+
const updatedFiles = [];
|
|
372
|
+
|
|
373
|
+
// Use pre-scanned files if available, otherwise scan
|
|
374
|
+
const allFiles = options._cachedFiles || await this.scan(projectRoot, { includeHidden: true });
|
|
375
|
+
|
|
376
|
+
// 2. Build list of files being moved and their new paths
|
|
377
|
+
const movedFiles = [];
|
|
378
|
+
for (const file of allFiles) {
|
|
379
|
+
if (file.startsWith(fromPath + '/') || file === fromPath) {
|
|
380
|
+
const newPath = file.replace(fromPath, toPath);
|
|
381
|
+
movedFiles.push({ from: file, to: newPath });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const shouldRefactorLinks = movedFiles.some(moved => needsGlobalLinkRefactor(moved.from, moved.to));
|
|
386
|
+
if (shouldRefactorLinks) {
|
|
387
|
+
// 3. Update links in files NOT being moved that reference moved files
|
|
388
|
+
for (const file of allFiles) {
|
|
389
|
+
if (file.startsWith(fromPath + '/')) continue; // Skip files being moved
|
|
390
|
+
|
|
391
|
+
const fullPath = path.join(projectRoot, file);
|
|
392
|
+
let content;
|
|
393
|
+
try {
|
|
394
|
+
content = await fsPromises.readFile(fullPath, 'utf8');
|
|
395
|
+
} catch (e) {
|
|
396
|
+
console.warn(`[file] Could not read ${file} for directory refactoring:`, e.message);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Update all links to moved files
|
|
401
|
+
let updatedContent = content;
|
|
402
|
+
for (const moved of movedFiles) {
|
|
403
|
+
updatedContent = Links.refactor(updatedContent, [
|
|
404
|
+
{ from: moved.from, to: moved.to },
|
|
405
|
+
], file);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (updatedContent !== content) {
|
|
409
|
+
await fsPromises.writeFile(fullPath, updatedContent);
|
|
410
|
+
updatedFiles.push(file);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// 4. Update asset paths in files being moved
|
|
416
|
+
for (const moved of movedFiles) {
|
|
417
|
+
const fullPath = path.join(projectRoot, moved.from);
|
|
418
|
+
let content;
|
|
419
|
+
try {
|
|
420
|
+
content = await fsPromises.readFile(fullPath, 'utf8');
|
|
421
|
+
} catch (e) {
|
|
422
|
+
console.warn(`[file] Could not read ${moved.from} for asset path update:`, e.message);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const updatedContent = Assets.refactorPaths(
|
|
427
|
+
content,
|
|
428
|
+
moved.from,
|
|
429
|
+
moved.to,
|
|
430
|
+
ASSETS_DIR_NAME
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
if (updatedContent !== content) {
|
|
434
|
+
await fsPromises.writeFile(fullPath, updatedContent);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 5. Actually move the directory
|
|
439
|
+
await fsPromises.mkdir(path.dirname(fullToPath), { recursive: true });
|
|
440
|
+
await fsPromises.rename(fullFromPath, fullToPath);
|
|
441
|
+
|
|
442
|
+
// 6. Clean up empty directories
|
|
443
|
+
await this.removeEmptyDirs(path.dirname(fullFromPath), projectRoot);
|
|
444
|
+
|
|
445
|
+
// 7. Invalidate project cache (skip if called from batch operation)
|
|
446
|
+
if (!options._cachedFiles && this.projectService) {
|
|
447
|
+
this.projectService.invalidate(projectRoot);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return { movedFile: toPath, updatedFiles };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Delete a file or directory
|
|
455
|
+
*
|
|
456
|
+
* @param {string} filePath - Absolute path to delete
|
|
457
|
+
*/
|
|
458
|
+
async delete(filePath) {
|
|
459
|
+
const stat = await fsPromises.stat(filePath);
|
|
460
|
+
if (stat.isDirectory()) {
|
|
461
|
+
await fsPromises.rm(filePath, { recursive: true });
|
|
462
|
+
} else {
|
|
463
|
+
await fsPromises.unlink(filePath);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Read a file
|
|
469
|
+
*
|
|
470
|
+
* @param {string} filePath - Absolute path to read
|
|
471
|
+
* @returns {Promise<string>}
|
|
472
|
+
*/
|
|
473
|
+
async read(filePath) {
|
|
474
|
+
return fsPromises.readFile(filePath, 'utf8');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Write a file
|
|
479
|
+
*
|
|
480
|
+
* @param {string} filePath - Absolute path to write
|
|
481
|
+
* @param {string} content - Content to write
|
|
482
|
+
*/
|
|
483
|
+
async write(filePath, content) {
|
|
484
|
+
await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
|
|
485
|
+
await fsPromises.writeFile(filePath, content);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Check if a directory exists
|
|
490
|
+
*/
|
|
491
|
+
async dirExists(dir) {
|
|
492
|
+
try {
|
|
493
|
+
const stat = await fsPromises.stat(dir);
|
|
494
|
+
return stat.isDirectory();
|
|
495
|
+
} catch {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Remove empty directories up to a limit
|
|
502
|
+
*/
|
|
503
|
+
async removeEmptyDirs(dir, stopAt) {
|
|
504
|
+
while (dir !== stopAt && dir.startsWith(stopAt)) {
|
|
505
|
+
try {
|
|
506
|
+
const entries = await fsPromises.readdir(dir);
|
|
507
|
+
if (entries.length === 0) {
|
|
508
|
+
await fsPromises.rmdir(dir);
|
|
509
|
+
dir = path.dirname(dir);
|
|
510
|
+
} else {
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
} catch {
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export default FileService;
|