gsd-opencode 1.9.2 → 1.10.2

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 (59) hide show
  1. package/agents/gsd-debugger.md +5 -5
  2. package/agents/gsd-settings.md +476 -30
  3. package/bin/gsd-install.js +105 -0
  4. package/bin/gsd.js +352 -0
  5. package/{command → commands}/gsd/add-phase.md +1 -1
  6. package/{command → commands}/gsd/audit-milestone.md +1 -1
  7. package/{command → commands}/gsd/debug.md +3 -3
  8. package/{command → commands}/gsd/discuss-phase.md +1 -1
  9. package/{command → commands}/gsd/execute-phase.md +1 -1
  10. package/{command → commands}/gsd/list-phase-assumptions.md +1 -1
  11. package/{command → commands}/gsd/map-codebase.md +1 -1
  12. package/{command → commands}/gsd/new-milestone.md +1 -1
  13. package/{command → commands}/gsd/new-project.md +3 -3
  14. package/{command → commands}/gsd/plan-phase.md +2 -2
  15. package/{command → commands}/gsd/research-phase.md +1 -1
  16. package/{command → commands}/gsd/verify-work.md +1 -1
  17. package/get-shit-done/workflows/list-phase-assumptions.md +1 -1
  18. package/get-shit-done/workflows/verify-work.md +5 -5
  19. package/lib/constants.js +199 -0
  20. package/package.json +34 -20
  21. package/src/commands/check.js +329 -0
  22. package/src/commands/config.js +337 -0
  23. package/src/commands/install.js +608 -0
  24. package/src/commands/list.js +256 -0
  25. package/src/commands/repair.js +519 -0
  26. package/src/commands/uninstall.js +732 -0
  27. package/src/commands/update.js +444 -0
  28. package/src/services/backup-manager.js +585 -0
  29. package/src/services/config.js +262 -0
  30. package/src/services/file-ops.js +855 -0
  31. package/src/services/health-checker.js +475 -0
  32. package/src/services/manifest-manager.js +301 -0
  33. package/src/services/migration-service.js +831 -0
  34. package/src/services/repair-service.js +846 -0
  35. package/src/services/scope-manager.js +303 -0
  36. package/src/services/settings.js +553 -0
  37. package/src/services/structure-detector.js +240 -0
  38. package/src/services/update-service.js +863 -0
  39. package/src/utils/hash.js +71 -0
  40. package/src/utils/interactive.js +222 -0
  41. package/src/utils/logger.js +128 -0
  42. package/src/utils/npm-registry.js +255 -0
  43. package/src/utils/path-resolver.js +226 -0
  44. /package/{command → commands}/gsd/add-todo.md +0 -0
  45. /package/{command → commands}/gsd/check-todos.md +0 -0
  46. /package/{command → commands}/gsd/complete-milestone.md +0 -0
  47. /package/{command → commands}/gsd/help.md +0 -0
  48. /package/{command → commands}/gsd/insert-phase.md +0 -0
  49. /package/{command → commands}/gsd/pause-work.md +0 -0
  50. /package/{command → commands}/gsd/plan-milestone-gaps.md +0 -0
  51. /package/{command → commands}/gsd/progress.md +0 -0
  52. /package/{command → commands}/gsd/quick.md +0 -0
  53. /package/{command → commands}/gsd/remove-phase.md +0 -0
  54. /package/{command → commands}/gsd/resume-work.md +0 -0
  55. /package/{command → commands}/gsd/set-model.md +0 -0
  56. /package/{command → commands}/gsd/set-profile.md +0 -0
  57. /package/{command → commands}/gsd/settings.md +0 -0
  58. /package/{command → commands}/gsd/update.md +0 -0
  59. /package/{command → commands}/gsd/whats-new.md +0 -0
@@ -0,0 +1,585 @@
1
+ /**
2
+ * Backup manager service for safe backup and retention during repair operations.
3
+ *
4
+ * This module provides backup creation and retention management for GSD-OpenCode
5
+ * installations. It handles:
6
+ * - Creating date-stamped backups of files before they are overwritten
7
+ * - Maintaining a backup directory with proper structure
8
+ * - Enforcing retention policies (keeping only N most recent backups)
9
+ * - Graceful error handling that doesn't fail the entire operation
10
+ *
11
+ * Used by the RepairService to ensure users can recover if repairs go wrong.
12
+ *
13
+ * @module backup-manager
14
+ */
15
+
16
+ import fs from 'fs/promises';
17
+ import path from 'path';
18
+
19
+ /**
20
+ * Manages backups for GSD-OpenCode installation repairs.
21
+ *
22
+ * This class provides safe backup creation with date-stamped filenames and
23
+ * automatic retention management. It ensures that files are backed up before
24
+ * being overwritten during repair operations, with configurable retention
25
+ * policies to prevent disk space issues.
26
+ *
27
+ * @class BackupManager
28
+ * @example
29
+ * const scope = new ScopeManager({ scope: 'global' });
30
+ * const logger = createLogger();
31
+ * const backupManager = new BackupManager(scope, logger, { maxBackups: 5 });
32
+ *
33
+ * // Create a backup before repair
34
+ * const result = await backupManager.backupFile(
35
+ * '/home/user/.config/opencode/agents/ro-commit.md',
36
+ * 'agents/ro-commit.md'
37
+ * );
38
+ * if (result.success) {
39
+ * console.log(`Backed up to: ${result.backupPath}`);
40
+ * }
41
+ *
42
+ * // Clean up old backups (keep only 5 newest)
43
+ * const cleanup = await backupManager.cleanupOldBackups();
44
+ * console.log(`Removed ${cleanup.cleaned} old backups`);
45
+ */
46
+ export class BackupManager {
47
+ /**
48
+ * Creates a new BackupManager instance.
49
+ *
50
+ * @param {Object} scopeManager - ScopeManager instance for path resolution
51
+ * @param {Object} logger - Logger instance for output (from logger.js)
52
+ * @param {Object} [options={}] - Configuration options
53
+ * @param {number} [options.maxBackups=5] - Maximum number of backups to retain
54
+ * @throws {Error} If scopeManager is not provided or missing getTargetDir method
55
+ * @throws {Error} If logger is not provided or missing required methods
56
+ *
57
+ * @example
58
+ * const scope = new ScopeManager({ scope: 'global' });
59
+ * const logger = createLogger();
60
+ * const backupManager = new BackupManager(scope, logger);
61
+ *
62
+ * // With custom retention
63
+ * const backupManager = new BackupManager(scope, logger, { maxBackups: 10 });
64
+ */
65
+ constructor(scopeManager, logger, options = {}) {
66
+ // Validate scopeManager
67
+ if (!scopeManager) {
68
+ throw new Error('ScopeManager instance is required');
69
+ }
70
+
71
+ if (typeof scopeManager.getTargetDir !== 'function') {
72
+ throw new Error('Invalid ScopeManager: missing getTargetDir method');
73
+ }
74
+
75
+ // Validate logger
76
+ if (!logger) {
77
+ throw new Error('Logger instance is required');
78
+ }
79
+
80
+ if (typeof logger.debug !== 'function') {
81
+ throw new Error('Invalid Logger: missing debug method');
82
+ }
83
+
84
+ if (typeof logger.info !== 'function') {
85
+ throw new Error('Invalid Logger: missing info method');
86
+ }
87
+
88
+ if (typeof logger.error !== 'function') {
89
+ throw new Error('Invalid Logger: missing error method');
90
+ }
91
+
92
+ this.scopeManager = scopeManager;
93
+ this.logger = logger;
94
+
95
+ // Set backup directory (within target installation directory)
96
+ const targetDir = scopeManager.getTargetDir();
97
+ this._backupDir = path.join(targetDir, '.backups');
98
+
99
+ // Set retention count (default to 5)
100
+ this._retentionCount = options.maxBackups ?? 5;
101
+
102
+ this.logger.debug(`BackupManager initialized with retention: ${this._retentionCount}`);
103
+ }
104
+
105
+ /**
106
+ * Creates a date-stamped backup of a file.
107
+ *
108
+ * Copies the source file to the backup directory with a date prefix.
109
+ * The backup filename format is: YYYY-MM-DD_original-filename.ext
110
+ *
111
+ * @param {string} sourcePath - Absolute path to the file being backed up
112
+ * @param {string} relativePath - Path relative to installation root (for reference)
113
+ * @returns {Promise<Object>} Backup result
114
+ * @property {boolean} success - True if backup was successful
115
+ * @property {string|null} backupPath - Path to the backup file, or null if failed
116
+ * @property {string} originalPath - Original file path
117
+ * @property {string|null} error - Error message if backup failed
118
+ *
119
+ * @example
120
+ * const result = await backupManager.backupFile(
121
+ * '/home/user/.config/opencode/agents/ro-commit.md',
122
+ * 'agents/ro-commit.md'
123
+ * );
124
+ * // Result: { success: true, backupPath: '/.../.backups/2026-02-10_ro-commit.md', ... }
125
+ */
126
+ async backupFile(sourcePath, relativePath) {
127
+ try {
128
+ // Check if source file exists
129
+ try {
130
+ await fs.access(sourcePath);
131
+ } catch (error) {
132
+ if (error.code === 'ENOENT') {
133
+ this.logger.debug(`No backup needed for ${relativePath}: file doesn't exist`);
134
+ return {
135
+ success: true,
136
+ backupPath: null,
137
+ originalPath: sourcePath,
138
+ note: 'File did not exist, no backup needed'
139
+ };
140
+ }
141
+ throw error;
142
+ }
143
+
144
+ // Ensure backup directory exists
145
+ await fs.mkdir(this._backupDir, { recursive: true });
146
+
147
+ // Generate date-stamped backup filename
148
+ const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
149
+ const originalName = path.basename(relativePath);
150
+ const backupFileName = `${timestamp}_${originalName}`;
151
+ const backupPath = path.join(this._backupDir, backupFileName);
152
+
153
+ this.logger.debug(`Creating backup: ${relativePath} -> ${backupFileName}`);
154
+
155
+ // Copy file to backup location
156
+ await fs.copyFile(sourcePath, backupPath);
157
+
158
+ this.logger.info(`Backed up ${relativePath}`);
159
+
160
+ return {
161
+ success: true,
162
+ backupPath,
163
+ originalPath: sourcePath
164
+ };
165
+ } catch (error) {
166
+ const errorMessage = `Failed to backup ${relativePath}: ${error.message}`;
167
+ this.logger.error(errorMessage);
168
+
169
+ return {
170
+ success: false,
171
+ backupPath: null,
172
+ originalPath: sourcePath,
173
+ error: errorMessage
174
+ };
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Cleans up old backups according to retention policy.
180
+ *
181
+ * Reads all backup files, sorts by date (newest first), and removes
182
+ * files beyond the retention count. Files without valid date prefixes
183
+ * are ignored and not deleted.
184
+ *
185
+ * @returns {Promise<Object>} Cleanup result
186
+ * @property {number} cleaned - Number of backups removed
187
+ * @property {number} kept - Number of backups retained
188
+ * @property {Array<string>} errors - Array of error messages for failed deletions
189
+ *
190
+ * @example
191
+ * const result = await backupManager.cleanupOldBackups();
192
+ * console.log(`Cleaned ${result.cleaned} backups, kept ${result.kept}`);
193
+ * if (result.errors.length > 0) {
194
+ * console.warn('Some backups could not be removed:', result.errors);
195
+ * }
196
+ */
197
+ async cleanupOldBackups() {
198
+ const errors = [];
199
+ let cleaned = 0;
200
+ let kept = 0;
201
+
202
+ try {
203
+ // Check if backup directory exists
204
+ try {
205
+ await fs.access(this._backupDir);
206
+ } catch (error) {
207
+ if (error.code === 'ENOENT') {
208
+ this.logger.debug('Backup directory does not exist, nothing to clean up');
209
+ return { cleaned: 0, kept: 0, errors: [] };
210
+ }
211
+ throw error;
212
+ }
213
+
214
+ // Read backup directory contents
215
+ const entries = await fs.readdir(this._backupDir, { withFileTypes: true });
216
+
217
+ // Filter for files (not directories) with date prefix
218
+ const backupFiles = entries
219
+ .filter(entry => entry.isFile())
220
+ .map(entry => entry.name)
221
+ .filter(name => this._isDatePrefixed(name));
222
+
223
+ if (backupFiles.length === 0) {
224
+ this.logger.debug('No backup files found in backup directory');
225
+ return { cleaned: 0, kept: 0, errors: [] };
226
+ }
227
+
228
+ this.logger.debug(`Found ${backupFiles.length} backup files, retention: ${this._retentionCount}`);
229
+
230
+ // Sort by date descending (newest first)
231
+ const sortedFiles = backupFiles.sort((a, b) => {
232
+ const dateA = this._extractDate(a);
233
+ const dateB = this._extractDate(b);
234
+ return dateB - dateA; // Descending order
235
+ });
236
+
237
+ // Remove files beyond retention count
238
+ const filesToRemove = sortedFiles.slice(this._retentionCount);
239
+ const filesToKeep = sortedFiles.slice(0, this._retentionCount);
240
+
241
+ kept = filesToKeep.length;
242
+
243
+ for (const fileName of filesToRemove) {
244
+ const filePath = path.join(this._backupDir, fileName);
245
+ try {
246
+ await fs.unlink(filePath);
247
+ cleaned++;
248
+ this.logger.debug(`Removed old backup: ${fileName}`);
249
+ } catch (error) {
250
+ const errorMsg = `Failed to remove ${fileName}: ${error.message}`;
251
+ errors.push(errorMsg);
252
+ this.logger.error(errorMsg);
253
+ }
254
+ }
255
+
256
+ if (cleaned > 0) {
257
+ this.logger.info(`Cleaned up ${cleaned} old backups, kept ${kept}`);
258
+ }
259
+
260
+ return { cleaned, kept, errors };
261
+ } catch (error) {
262
+ const errorMessage = `Failed to cleanup old backups: ${error.message}`;
263
+ this.logger.error(errorMessage);
264
+ throw new Error(errorMessage);
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Checks if a filename has a date prefix (YYYY-MM-DD format).
270
+ *
271
+ * @param {string} fileName - Filename to check
272
+ * @returns {boolean} True if filename starts with date pattern
273
+ * @private
274
+ */
275
+ _isDatePrefixed(fileName) {
276
+ // Check if filename starts with YYYY-MM-DD pattern
277
+ const datePattern = /^\d{4}-\d{2}-\d{2}_/;
278
+ return datePattern.test(fileName);
279
+ }
280
+
281
+ /**
282
+ * Extracts the date from a backup filename.
283
+ *
284
+ * @param {string} fileName - Backup filename with date prefix
285
+ * @returns {Date} Date object parsed from filename
286
+ * @private
287
+ */
288
+ _extractDate(fileName) {
289
+ // Extract YYYY-MM-DD from beginning of filename
290
+ const dateString = fileName.substring(0, 10);
291
+ return new Date(dateString);
292
+ }
293
+
294
+ /**
295
+ * Returns the backup directory path.
296
+ *
297
+ * @returns {string} Path to backup directory
298
+ */
299
+ getBackupDir() {
300
+ return this._backupDir;
301
+ }
302
+
303
+ /**
304
+ * Returns the current retention count.
305
+ *
306
+ * @returns {number} Number of backups to retain
307
+ */
308
+ getRetentionCount() {
309
+ return this._retentionCount;
310
+ }
311
+
312
+ /**
313
+ * Creates a migration backup of the entire installation.
314
+ *
315
+ * Creates a timestamped backup of the installation before migration,
316
+ * including manifest, command/gsd/ directory, and all tracked files.
317
+ * Stores backup in .backups/ subdirectory.
318
+ *
319
+ * @param {Object} options - Backup options
320
+ * @param {string} options.targetDir - Path to the installation directory
321
+ * @param {string} options.originalStructure - Structure type ('old', 'new', 'dual', 'none')
322
+ * @param {number} options.timestamp - Timestamp for the backup
323
+ * @returns {Promise<Object>} Backup metadata
324
+ * @property {string} path - Path to the backup directory
325
+ * @property {number} timestamp - Backup timestamp
326
+ * @property {string} originalStructure - Original structure type
327
+ * @property {string[]} affectedFiles - List of files that will be affected
328
+ * @property {Object|null} originalManifest - Original manifest content (if exists)
329
+ *
330
+ * @example
331
+ * const backup = await backupManager.createMigrationBackup({
332
+ * targetDir: '/home/user/.config/opencode',
333
+ * originalStructure: 'old',
334
+ * timestamp: Date.now()
335
+ * });
336
+ * // Returns: { path: '/.../.backups/backup-1234567890', ... }
337
+ */
338
+ async createMigrationBackup({ targetDir, originalStructure, timestamp }) {
339
+ const backupDirName = `backup-${timestamp}`;
340
+ const migrationBackupDir = path.join(targetDir, '.backups', backupDirName);
341
+
342
+ this.logger.info('Creating migration backup...');
343
+
344
+ try {
345
+ // Ensure migration backup directory exists
346
+ await fs.mkdir(migrationBackupDir, { recursive: true });
347
+
348
+ // Determine files to backup based on structure type
349
+ const affectedFiles = [];
350
+ const oldPath = path.join(targetDir, 'command', 'gsd');
351
+ const newPath = path.join(targetDir, 'commands', 'gsd');
352
+ const manifestPath = path.join(targetDir, 'get-shit-done', 'INSTALLED_FILES.json');
353
+
354
+ // Check and add old structure files
355
+ try {
356
+ await fs.access(oldPath);
357
+ const oldFiles = await this._collectFilesRecursively(oldPath);
358
+ for (const file of oldFiles) {
359
+ const relativePath = path.relative(targetDir, file);
360
+ affectedFiles.push(relativePath);
361
+ await this._copyToBackup(file, migrationBackupDir, relativePath);
362
+ }
363
+ } catch (error) {
364
+ if (error.code !== 'ENOENT') {
365
+ throw error;
366
+ }
367
+ }
368
+
369
+ // Check and add new structure files
370
+ try {
371
+ await fs.access(newPath);
372
+ const newFiles = await this._collectFilesRecursively(newPath);
373
+ for (const file of newFiles) {
374
+ const relativePath = path.relative(targetDir, file);
375
+ if (!affectedFiles.includes(relativePath)) {
376
+ affectedFiles.push(relativePath);
377
+ await this._copyToBackup(file, migrationBackupDir, relativePath);
378
+ }
379
+ }
380
+ } catch (error) {
381
+ if (error.code !== 'ENOENT') {
382
+ throw error;
383
+ }
384
+ }
385
+
386
+ // Backup manifest if it exists
387
+ let originalManifest = null;
388
+ try {
389
+ await fs.access(manifestPath);
390
+ const manifestContent = await fs.readFile(manifestPath, 'utf-8');
391
+ originalManifest = JSON.parse(manifestContent);
392
+ await fs.writeFile(
393
+ path.join(migrationBackupDir, 'manifest.json'),
394
+ manifestContent,
395
+ 'utf-8'
396
+ );
397
+ affectedFiles.push('get-shit-done/INSTALLED_FILES.json');
398
+ } catch (error) {
399
+ if (error.code !== 'ENOENT') {
400
+ throw error;
401
+ }
402
+ this.logger.debug('No manifest found, skipping manifest backup');
403
+ }
404
+
405
+ // Create backup metadata file
406
+ const metadata = {
407
+ timestamp,
408
+ originalStructure,
409
+ affectedFiles,
410
+ backupPath: migrationBackupDir,
411
+ created: new Date(timestamp).toISOString()
412
+ };
413
+ await fs.writeFile(
414
+ path.join(migrationBackupDir, 'metadata.json'),
415
+ JSON.stringify(metadata, null, 2),
416
+ 'utf-8'
417
+ );
418
+
419
+ this.logger.info(`Migration backup created at ${migrationBackupDir}`);
420
+ this.logger.info(`Backed up ${affectedFiles.length} files`);
421
+
422
+ return {
423
+ path: migrationBackupDir,
424
+ timestamp,
425
+ originalStructure,
426
+ affectedFiles,
427
+ originalManifest
428
+ };
429
+ } catch (error) {
430
+ // Clean up partial backup on failure
431
+ try {
432
+ await fs.rm(migrationBackupDir, { recursive: true, force: true });
433
+ } catch {
434
+ // Ignore cleanup errors
435
+ }
436
+ throw new Error(`Failed to create migration backup: ${error.message}`);
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Restores original structure from migration backup.
442
+ *
443
+ * Handles rollback of failed migrations by restoring files from backup
444
+ * and cleaning up any partial migration artifacts.
445
+ *
446
+ * @param {Object} backup - Backup metadata object from createMigrationBackup
447
+ * @returns {Promise<boolean>} True if restore succeeded
448
+ *
449
+ * @example
450
+ * const success = await backupManager.restoreFromMigrationBackup(backup);
451
+ * if (success) {
452
+ * console.log('Rollback completed successfully');
453
+ * }
454
+ */
455
+ async restoreFromMigrationBackup(backup) {
456
+ if (!backup || !backup.path) {
457
+ throw new Error('Invalid backup metadata: path is required');
458
+ }
459
+
460
+ this.logger.info('Restoring from migration backup...');
461
+
462
+ try {
463
+ const backupDir = backup.path;
464
+ const targetDir = this.scopeManager.getTargetDir();
465
+
466
+ // Verify backup directory exists
467
+ try {
468
+ await fs.access(backupDir);
469
+ } catch (error) {
470
+ throw new Error(`Backup directory not found: ${backupDir}`);
471
+ }
472
+
473
+ // Load metadata
474
+ let metadata;
475
+ try {
476
+ const metadataContent = await fs.readFile(
477
+ path.join(backupDir, 'metadata.json'),
478
+ 'utf-8'
479
+ );
480
+ metadata = JSON.parse(metadataContent);
481
+ } catch (error) {
482
+ this.logger.warning('Could not load backup metadata, proceeding with file restoration');
483
+ metadata = { affectedFiles: [] };
484
+ }
485
+
486
+ // Restore manifest if backed up
487
+ const manifestBackupPath = path.join(backupDir, 'manifest.json');
488
+ const manifestTargetPath = path.join(targetDir, 'get-shit-done', 'INSTALLED_FILES.json');
489
+ try {
490
+ await fs.access(manifestBackupPath);
491
+ await fs.mkdir(path.dirname(manifestTargetPath), { recursive: true });
492
+ await fs.copyFile(manifestBackupPath, manifestTargetPath);
493
+ this.logger.debug('Restored manifest');
494
+ } catch (error) {
495
+ if (error.code !== 'ENOENT') {
496
+ throw error;
497
+ }
498
+ }
499
+
500
+ // Restore files from backup
501
+ for (const relativePath of metadata.affectedFiles || []) {
502
+ if (relativePath === 'get-shit-done/INSTALLED_FILES.json') {
503
+ continue; // Already handled above
504
+ }
505
+
506
+ const backupFilePath = path.join(backupDir, relativePath);
507
+ const targetFilePath = path.join(targetDir, relativePath);
508
+
509
+ try {
510
+ await fs.access(backupFilePath);
511
+ await fs.mkdir(path.dirname(targetFilePath), { recursive: true });
512
+ await fs.copyFile(backupFilePath, targetFilePath);
513
+ this.logger.debug(`Restored: ${relativePath}`);
514
+ } catch (error) {
515
+ if (error.code !== 'ENOENT') {
516
+ this.logger.error(`Failed to restore ${relativePath}: ${error.message}`);
517
+ }
518
+ }
519
+ }
520
+
521
+ this.logger.success('Migration rollback completed');
522
+ return true;
523
+ } catch (error) {
524
+ this.logger.error(`Migration rollback failed: ${error.message}`);
525
+ return false;
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Collects all files recursively in a directory.
531
+ *
532
+ * @param {string} dirPath - Directory to scan
533
+ * @returns {Promise<string[]>} Array of file paths
534
+ * @private
535
+ */
536
+ async _collectFilesRecursively(dirPath) {
537
+ const files = [];
538
+
539
+ try {
540
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
541
+
542
+ for (const entry of entries) {
543
+ const fullPath = path.join(dirPath, entry.name);
544
+ if (entry.isDirectory()) {
545
+ const subFiles = await this._collectFilesRecursively(fullPath);
546
+ files.push(...subFiles);
547
+ } else {
548
+ files.push(fullPath);
549
+ }
550
+ }
551
+ } catch (error) {
552
+ if (error.code !== 'ENOENT') {
553
+ throw error;
554
+ }
555
+ }
556
+
557
+ return files;
558
+ }
559
+
560
+ /**
561
+ * Copies a file to the backup directory maintaining relative path structure.
562
+ *
563
+ * @param {string} sourcePath - Source file path
564
+ * @param {string} backupDir - Backup directory root
565
+ * @param {string} relativePath - Relative path from target dir
566
+ * @returns {Promise<void>}
567
+ * @private
568
+ */
569
+ async _copyToBackup(sourcePath, backupDir, relativePath) {
570
+ const targetPath = path.join(backupDir, relativePath);
571
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
572
+ await fs.copyFile(sourcePath, targetPath);
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Default export for the backup-manager module.
578
+ *
579
+ * @example
580
+ * import { BackupManager } from './services/backup-manager.js';
581
+ * const backupManager = new BackupManager(scopeManager, logger);
582
+ */
583
+ export default {
584
+ BackupManager
585
+ };