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,846 @@
1
+ /**
2
+ * Repair service for detecting and fixing installation issues.
3
+ *
4
+ * This module provides the core repair logic that detects issues and fixes them
5
+ * safely with backups and progress reporting. It orchestrates detection, backup,
6
+ * and repair operations for broken GSD-OpenCode installations.
7
+ *
8
+ * Works in conjunction with HealthChecker for issue detection, BackupManager for
9
+ * safe backups before destructive operations, and FileOperations for file reinstall.
10
+ *
11
+ * @module repair-service
12
+ */
13
+
14
+ import fs from 'fs/promises';
15
+ import path from 'path';
16
+ import { fileURLToPath } from 'url';
17
+ import { ScopeManager } from './scope-manager.js';
18
+ import { BackupManager } from './backup-manager.js';
19
+ import { FileOperations } from './file-ops.js';
20
+ import { MigrationService } from './migration-service.js';
21
+ import { StructureDetector, STRUCTURE_TYPES } from './structure-detector.js';
22
+ import { PATH_PATTERNS } from '../../lib/constants.js';
23
+
24
+ // Get the directory of the current module for resolving source paths
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = path.dirname(__filename);
27
+
28
+ /**
29
+ * Manages repair operations for GSD-OpenCode installations.
30
+ *
31
+ * This class provides methods to detect installation issues (missing files,
32
+ * corrupted files, path issues) and repair them safely with backup creation
33
+ * and progress reporting. It uses a two-phase repair strategy: first fixing
34
+ * non-destructive issues (missing files), then destructive issues (corrupted
35
+ * files, path issues) with proper backups.
36
+ *
37
+ * @class RepairService
38
+ * @example
39
+ * const scope = new ScopeManager({ scope: 'global' });
40
+ * const backupManager = new BackupManager(scope, logger);
41
+ * const fileOps = new FileOperations(scope, logger);
42
+ * const repairService = new RepairService({
43
+ * scopeManager: scope,
44
+ * backupManager: backupManager,
45
+ * fileOps: fileOps,
46
+ * logger: logger,
47
+ * expectedVersion: '1.0.0'
48
+ * });
49
+ *
50
+ * // Detect issues
51
+ * const issues = await repairService.detectIssues();
52
+ * if (issues.hasIssues) {
53
+ * console.log(repairService.generateSummary(issues));
54
+ *
55
+ * // Repair with progress tracking
56
+ * const result = await repairService.repair(issues, {
57
+ * onProgress: ({ current, total, operation, file }) => {
58
+ * console.log(`${operation}: ${current}/${total} - ${file}`);
59
+ * }
60
+ * });
61
+ *
62
+ * console.log(`Repairs: ${result.stats.succeeded}/${result.stats.total} succeeded`);
63
+ * }
64
+ */
65
+ export class RepairService {
66
+ /**
67
+ * Creates a new RepairService instance.
68
+ *
69
+ * @param {Object} dependencies - Required dependencies
70
+ * @param {ScopeManager} dependencies.scopeManager - ScopeManager instance for path resolution
71
+ * @param {BackupManager} dependencies.backupManager - BackupManager instance for creating backups
72
+ * @param {FileOperations} dependencies.fileOps - FileOperations instance for file reinstall
73
+ * @param {Object} dependencies.logger - Logger instance for output
74
+ * @param {string} dependencies.expectedVersion - Expected version string for version checks
75
+ * @throws {Error} If any required dependency is missing or invalid
76
+ *
77
+ * @example
78
+ * const repairService = new RepairService({
79
+ * scopeManager: scope,
80
+ * backupManager: backupManager,
81
+ * fileOps: fileOps,
82
+ * logger: logger,
83
+ * expectedVersion: '1.0.0'
84
+ * });
85
+ */
86
+ constructor(dependencies) {
87
+ // Validate all required dependencies
88
+ if (!dependencies) {
89
+ throw new Error('Dependencies object is required');
90
+ }
91
+
92
+ const { scopeManager, backupManager, fileOps, logger, expectedVersion } = dependencies;
93
+
94
+ // Validate scopeManager
95
+ if (!scopeManager) {
96
+ throw new Error('ScopeManager instance is required');
97
+ }
98
+ if (typeof scopeManager.getTargetDir !== 'function') {
99
+ throw new Error('Invalid ScopeManager: missing getTargetDir method');
100
+ }
101
+
102
+ // Validate backupManager
103
+ if (!backupManager) {
104
+ throw new Error('BackupManager instance is required');
105
+ }
106
+ if (typeof backupManager.backupFile !== 'function') {
107
+ throw new Error('Invalid BackupManager: missing backupFile method');
108
+ }
109
+
110
+ // Validate fileOps
111
+ if (!fileOps) {
112
+ throw new Error('FileOperations instance is required');
113
+ }
114
+ if (typeof fileOps._copyFile !== 'function') {
115
+ throw new Error('Invalid FileOperations: missing _copyFile method');
116
+ }
117
+
118
+ // Validate logger
119
+ if (!logger) {
120
+ throw new Error('Logger instance is required');
121
+ }
122
+ if (typeof logger.info !== 'function' || typeof logger.error !== 'function') {
123
+ throw new Error('Invalid Logger: missing required methods (info, error)');
124
+ }
125
+
126
+ // Validate expectedVersion
127
+ if (!expectedVersion || typeof expectedVersion !== 'string') {
128
+ throw new Error('Expected version must be a non-empty string');
129
+ }
130
+
131
+ // Store dependencies
132
+ this.scopeManager = scopeManager;
133
+ this.backupManager = backupManager;
134
+ this.fileOps = fileOps;
135
+ this.logger = logger;
136
+ this.expectedVersion = expectedVersion;
137
+
138
+ // Initialize structure detector for migration support
139
+ this.structureDetector = new StructureDetector(this.scopeManager.getTargetDir());
140
+
141
+ // Lazy-load HealthChecker to avoid circular dependencies
142
+ this._healthChecker = null;
143
+
144
+ this.logger.debug('RepairService initialized');
145
+ }
146
+
147
+ /**
148
+ * Checks the directory structure status.
149
+ *
150
+ * Detects which command directory structure is present (old/new/dual/none)
151
+ * and determines if repair is needed.
152
+ *
153
+ * @returns {Promise<Object>} Structure check results
154
+ * @property {string} type - One of STRUCTURE_TYPES values
155
+ * @property {boolean} canRepair - True if structure can be repaired
156
+ * @property {string|null} repairCommand - Command to run for repair, or null
157
+ * @property {boolean} needsMigration - True if migration is recommended
158
+ *
159
+ * @example
160
+ * const structureCheck = await repairService.checkStructure();
161
+ * if (structureCheck.needsMigration) {
162
+ * console.log(`Run: ${structureCheck.repairCommand}`);
163
+ * }
164
+ */
165
+ async checkStructure() {
166
+ const structure = await this.structureDetector.detect();
167
+ const details = await this.structureDetector.getDetails();
168
+
169
+ const canRepair = structure === STRUCTURE_TYPES.OLD ||
170
+ structure === STRUCTURE_TYPES.DUAL;
171
+
172
+ return {
173
+ type: structure,
174
+ canRepair,
175
+ repairCommand: canRepair ? 'gsd-opencode repair --fix-structure' : null,
176
+ needsMigration: canRepair,
177
+ details: {
178
+ oldExists: details.oldExists,
179
+ newExists: details.newExists,
180
+ recommendedAction: details.recommendedAction
181
+ }
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Repairs the directory structure by migrating to new format.
187
+ *
188
+ * Uses MigrationService to perform atomic migration from old to new
189
+ * structure with full backup and rollback capability.
190
+ *
191
+ * @returns {Promise<Object>} Repair result
192
+ * @property {boolean} repaired - True if structure was repaired
193
+ * @property {string} message - Human-readable status message
194
+ * @property {string} [backup] - Path to backup if repair performed
195
+ * @property {Error} [error] - Error if repair failed
196
+ *
197
+ * @example
198
+ * const result = await repairService.repairStructure();
199
+ * if (result.repaired) {
200
+ * console.log('Structure repaired successfully');
201
+ * console.log(`Backup: ${result.backup}`);
202
+ * } else {
203
+ * console.log(`No repair needed: ${result.message}`);
204
+ * }
205
+ */
206
+ async repairStructure() {
207
+ const structure = await this.structureDetector.detect();
208
+
209
+ if (structure === STRUCTURE_TYPES.NEW) {
210
+ return {
211
+ repaired: false,
212
+ message: 'Already using new structure (commands/gsd/)'
213
+ };
214
+ }
215
+
216
+ if (structure === STRUCTURE_TYPES.NONE) {
217
+ return {
218
+ repaired: false,
219
+ message: 'No structure to repair - fresh installation needed'
220
+ };
221
+ }
222
+
223
+ // Use MigrationService to fix structure
224
+ const migrationService = new MigrationService(this.scopeManager, this.logger);
225
+
226
+ try {
227
+ const result = await migrationService.migrate();
228
+
229
+ if (result.migrated) {
230
+ return {
231
+ repaired: true,
232
+ message: 'Structure repaired successfully - migrated to commands/gsd/',
233
+ backup: result.backup
234
+ };
235
+ } else {
236
+ return {
237
+ repaired: false,
238
+ message: `No repair needed: ${result.reason}`
239
+ };
240
+ }
241
+ } catch (error) {
242
+ return {
243
+ repaired: false,
244
+ message: `Repair failed: ${error.message}`,
245
+ error
246
+ };
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Fixes dual structure state (both old and new exist).
252
+ *
253
+ * This handles interrupted migrations by consolidating to new structure.
254
+ * Verifies new structure is complete, then removes old structure.
255
+ *
256
+ * @returns {Promise<Object>} Fix result
257
+ * @property {boolean} fixed - True if dual structure was fixed
258
+ * @property {string} message - Status message
259
+ * @property {string} [backup] - Backup path if created
260
+ * @property {Error} [error] - Error if fix failed
261
+ *
262
+ * @example
263
+ * const result = await repairService.fixDualStructure();
264
+ * if (result.fixed) {
265
+ * console.log('Dual structure fixed');
266
+ * }
267
+ */
268
+ async fixDualStructure() {
269
+ const structure = await this.structureDetector.detect();
270
+
271
+ if (structure !== STRUCTURE_TYPES.DUAL) {
272
+ return {
273
+ fixed: false,
274
+ message: `Not in dual structure state (current: ${structure})`
275
+ };
276
+ }
277
+
278
+ this.logger.info('Fixing dual structure - consolidating to new structure...');
279
+
280
+ // Delegate to migration service which handles dual state
281
+ const migrationService = new MigrationService(this.scopeManager, this.logger);
282
+
283
+ try {
284
+ const result = await migrationService.migrate();
285
+
286
+ if (result.migrated) {
287
+ return {
288
+ fixed: true,
289
+ message: 'Dual structure fixed - removed old command/gsd/ directory',
290
+ backup: result.backup
291
+ };
292
+ } else {
293
+ return {
294
+ fixed: false,
295
+ message: 'Could not fix dual structure: migration returned no changes'
296
+ };
297
+ }
298
+ } catch (error) {
299
+ return {
300
+ fixed: false,
301
+ message: `Failed to fix dual structure: ${error.message}`,
302
+ error
303
+ };
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Gets or creates the HealthChecker instance.
309
+ *
310
+ * @returns {Promise<Object>} HealthChecker instance
311
+ * @private
312
+ */
313
+ async _getHealthChecker() {
314
+ if (!this._healthChecker) {
315
+ const { HealthChecker } = await import('./health-checker.js');
316
+ this._healthChecker = new HealthChecker(this.scopeManager);
317
+ }
318
+ return this._healthChecker;
319
+ }
320
+
321
+ /**
322
+ * Detects installation issues by running health checks.
323
+ *
324
+ * Uses HealthChecker to verify file existence, version matching, and file
325
+ * integrity. Categorizes issues into missing files, corrupted files, and
326
+ * path issues. Does not modify any files during detection.
327
+ *
328
+ * @returns {Promise<Object>} Categorized issues
329
+ * @property {boolean} hasIssues - True if any issues were found
330
+ * @property {Array} missingFiles - Files/directories that don't exist
331
+ * @property {Array} corruptedFiles - Files that failed integrity checks
332
+ * @property {Array} pathIssues - .md files with incorrect @gsd-opencode/ references
333
+ * @property {number} totalIssues - Total count of all issues
334
+ *
335
+ * @example
336
+ * const issues = await repairService.detectIssues();
337
+ * console.log(issues.hasIssues); // true/false
338
+ * console.log(issues.missingFiles); // [{ path, type }]
339
+ * console.log(issues.corruptedFiles); // [{ path, relative, error }]
340
+ * console.log(issues.pathIssues); // [{ path, relative, currentContent }]
341
+ */
342
+ async detectIssues() {
343
+ this.logger.info('Detecting installation issues...');
344
+
345
+ const healthChecker = await this._getHealthChecker();
346
+ const targetDir = this.scopeManager.getTargetDir();
347
+
348
+ // Run all health checks
349
+ const checkResult = await healthChecker.checkAll({
350
+ expectedVersion: this.expectedVersion
351
+ });
352
+
353
+ // Categorize issues
354
+ const missingFiles = [];
355
+ const corruptedFiles = [];
356
+
357
+ // Parse file existence checks
358
+ if (checkResult.categories.files && !checkResult.categories.files.passed) {
359
+ for (const check of checkResult.categories.files.checks) {
360
+ if (!check.passed) {
361
+ const isDirectory = check.name.includes('directory');
362
+ missingFiles.push({
363
+ path: check.path,
364
+ type: isDirectory ? 'directory' : 'file',
365
+ name: check.name
366
+ });
367
+ }
368
+ }
369
+ }
370
+
371
+ // Parse integrity checks for corrupted files
372
+ if (checkResult.categories.integrity && !checkResult.categories.integrity.passed) {
373
+ for (const check of checkResult.categories.integrity.checks) {
374
+ if (!check.passed && check.error) {
375
+ // Only include actual file errors, not missing files (those go in missingFiles)
376
+ if (!check.error.includes('not found')) {
377
+ corruptedFiles.push({
378
+ path: check.file,
379
+ relative: check.relative,
380
+ error: check.error
381
+ });
382
+ }
383
+ }
384
+ }
385
+ }
386
+
387
+ // Detect path issues in .md files
388
+ const pathIssues = await this._detectPathIssues(targetDir);
389
+
390
+ const totalIssues = missingFiles.length + corruptedFiles.length + pathIssues.length;
391
+ const hasIssues = totalIssues > 0;
392
+
393
+ this.logger.info(`Found ${totalIssues} issue(s): ${missingFiles.length} missing, ${corruptedFiles.length} corrupted, ${pathIssues.length} path issues`);
394
+
395
+ return {
396
+ hasIssues,
397
+ missingFiles,
398
+ corruptedFiles,
399
+ pathIssues,
400
+ totalIssues
401
+ };
402
+ }
403
+
404
+ /**
405
+ * Detects path issues in .md files.
406
+ *
407
+ * Reads .md files and checks for @gsd-opencode/ pattern references.
408
+ * Compares expected path (targetDir + '/') with actual references.
409
+ *
410
+ * @param {string} targetDir - Target installation directory
411
+ * @returns {Promise<Array>} Array of path issues
412
+ * @private
413
+ */
414
+ async _detectPathIssues(targetDir) {
415
+ const pathIssues = [];
416
+
417
+ // Sample files to check for path issues (same as integrity check samples)
418
+ const sampleFiles = [
419
+ { dir: 'agents', file: 'gsd-executor.md' },
420
+ { dir: 'command', file: 'gsd/help.md' },
421
+ { dir: 'get-shit-done', file: 'templates/summary.md' }
422
+ ];
423
+
424
+ const expectedPrefix = targetDir + '/';
425
+
426
+ for (const { dir, file } of sampleFiles) {
427
+ const filePath = path.join(targetDir, dir, file);
428
+ const relativePath = path.join(dir, file);
429
+
430
+ try {
431
+ const content = await fs.readFile(filePath, 'utf-8');
432
+
433
+ // Check for @gsd-opencode/ references that haven't been replaced
434
+ const hasWrongReferences = PATH_PATTERNS.gsdReference.test(content);
435
+
436
+ if (hasWrongReferences) {
437
+ pathIssues.push({
438
+ path: filePath,
439
+ relative: relativePath,
440
+ currentContent: content
441
+ });
442
+ }
443
+ } catch (error) {
444
+ // File doesn't exist or can't be read - this is a missing file issue, not a path issue
445
+ this.logger.debug(`Could not check path issues for ${relativePath}: ${error.message}`);
446
+ }
447
+ }
448
+
449
+ return pathIssues;
450
+ }
451
+
452
+ /**
453
+ * Repairs detected issues with backup and progress reporting.
454
+ *
455
+ * Uses a two-phase repair strategy:
456
+ * - Phase 1: Fix non-destructive issues (missing files) - auto, no backup needed
457
+ * - Phase 2: Fix destructive issues (corrupted files, path issues) - backup first
458
+ *
459
+ * Continues with remaining repairs if one fails, collecting all results.
460
+ *
461
+ * @param {Object} issues - Issues object from detectIssues()
462
+ * @param {Array} issues.missingFiles - Missing files/directories to create
463
+ * @param {Array} issues.corruptedFiles - Corrupted files to replace
464
+ * @param {Array} issues.pathIssues - Files with path reference issues
465
+ * @param {Object} [options={}] - Repair options
466
+ * @param {Function} [options.onProgress] - Progress callback ({ current, total, operation, file })
467
+ * @param {Function} [options.onBackup] - Backup callback ({ file, backupPath })
468
+ * @returns {Promise<Object>} Repair results
469
+ * @property {boolean} success - True only if ALL repairs succeeded
470
+ * @property {Object} results - Detailed results by category
471
+ * @property {Object} stats - Summary statistics
472
+ *
473
+ * @example
474
+ * const result = await repairService.repair(issues, {
475
+ * onProgress: ({ current, total, operation, file }) => {
476
+ * console.log(`${operation}: ${current}/${total} - ${file}`);
477
+ * }
478
+ * });
479
+ *
480
+ * console.log(result.success); // true/false
481
+ * console.log(result.stats.succeeded + '/' + result.stats.total);
482
+ */
483
+ async repair(issues, options = {}) {
484
+ const { onProgress, onBackup } = options;
485
+
486
+ this.logger.info('Starting repair process...');
487
+
488
+ // Initialize results structure
489
+ const results = {
490
+ missing: [],
491
+ corrupted: [],
492
+ paths: []
493
+ };
494
+
495
+ // Calculate total operations
496
+ const totalOperations =
497
+ (issues.missingFiles?.length || 0) +
498
+ (issues.corruptedFiles?.length || 0) +
499
+ (issues.pathIssues?.length || 0);
500
+
501
+ if (totalOperations === 0) {
502
+ this.logger.info('No repairs needed');
503
+ return {
504
+ success: true,
505
+ results,
506
+ stats: {
507
+ total: 0,
508
+ succeeded: 0,
509
+ failed: 0,
510
+ byCategory: { missing: { succeeded: 0, failed: 0 }, corrupted: { succeeded: 0, failed: 0 }, paths: { succeeded: 0, failed: 0 } }
511
+ }
512
+ };
513
+ }
514
+
515
+ let currentOperation = 0;
516
+ let succeededCount = 0;
517
+ let failedCount = 0;
518
+
519
+ // Phase 1: Fix missing files (non-destructive)
520
+ for (const missingFile of (issues.missingFiles || [])) {
521
+ currentOperation++;
522
+
523
+ try {
524
+ await this._repairMissingFile(missingFile);
525
+ succeededCount++;
526
+ results.missing.push({
527
+ file: missingFile.path,
528
+ success: true
529
+ });
530
+ this.logger.info(`Fixed missing ${missingFile.type}: ${missingFile.name}`);
531
+ } catch (error) {
532
+ failedCount++;
533
+ results.missing.push({
534
+ file: missingFile.path,
535
+ success: false,
536
+ error: error.message
537
+ });
538
+ this.logger.error(`Failed to fix missing ${missingFile.type}: ${missingFile.name}`, error);
539
+ }
540
+
541
+ if (onProgress) {
542
+ onProgress({
543
+ current: currentOperation,
544
+ total: totalOperations,
545
+ operation: 'installing',
546
+ file: missingFile.name
547
+ });
548
+ }
549
+ }
550
+
551
+ // Phase 2: Fix corrupted files (destructive - backup first)
552
+ for (const corruptedFile of (issues.corruptedFiles || [])) {
553
+ currentOperation++;
554
+
555
+ try {
556
+ // Backup before replacing
557
+ const backupResult = await this.backupManager.backupFile(
558
+ corruptedFile.path,
559
+ corruptedFile.relative
560
+ );
561
+
562
+ if (onBackup && backupResult.backupPath) {
563
+ onBackup({
564
+ file: corruptedFile.relative,
565
+ backupPath: backupResult.backupPath
566
+ });
567
+ }
568
+
569
+ // Reinstall the file
570
+ const sourcePath = this._getSourcePath(corruptedFile.relative);
571
+ const targetPath = corruptedFile.path;
572
+ await this.fileOps._copyFile(sourcePath, targetPath);
573
+
574
+ succeededCount++;
575
+ results.corrupted.push({
576
+ file: corruptedFile.path,
577
+ success: true
578
+ });
579
+ this.logger.info(`Fixed corrupted file: ${corruptedFile.relative}`);
580
+ } catch (error) {
581
+ failedCount++;
582
+ results.corrupted.push({
583
+ file: corruptedFile.path,
584
+ success: false,
585
+ error: error.message
586
+ });
587
+ this.logger.error(`Failed to fix corrupted file: ${corruptedFile.relative}`, error);
588
+ }
589
+
590
+ if (onProgress) {
591
+ onProgress({
592
+ current: currentOperation,
593
+ total: totalOperations,
594
+ operation: 'replacing',
595
+ file: corruptedFile.relative
596
+ });
597
+ }
598
+ }
599
+
600
+ // Phase 3: Fix path issues (destructive - backup first)
601
+ for (const pathIssue of (issues.pathIssues || [])) {
602
+ currentOperation++;
603
+
604
+ try {
605
+ // Backup before modifying
606
+ const backupResult = await this.backupManager.backupFile(
607
+ pathIssue.path,
608
+ pathIssue.relative
609
+ );
610
+
611
+ if (onBackup && backupResult.backupPath) {
612
+ onBackup({
613
+ file: pathIssue.relative,
614
+ backupPath: backupResult.backupPath
615
+ });
616
+ }
617
+
618
+ // Fix path references
619
+ const targetDir = this.scopeManager.getTargetDir();
620
+ const updatedContent = pathIssue.currentContent.replace(
621
+ PATH_PATTERNS.gsdReference,
622
+ targetDir + '/'
623
+ );
624
+
625
+ await fs.writeFile(pathIssue.path, updatedContent, 'utf-8');
626
+
627
+ succeededCount++;
628
+ results.paths.push({
629
+ file: pathIssue.path,
630
+ success: true
631
+ });
632
+ this.logger.info(`Fixed path issues in: ${pathIssue.relative}`);
633
+ } catch (error) {
634
+ failedCount++;
635
+ results.paths.push({
636
+ file: pathIssue.path,
637
+ success: false,
638
+ error: error.message
639
+ });
640
+ this.logger.error(`Failed to fix path issues in: ${pathIssue.relative}`, error);
641
+ }
642
+
643
+ if (onProgress) {
644
+ onProgress({
645
+ current: currentOperation,
646
+ total: totalOperations,
647
+ operation: 'updating-paths',
648
+ file: pathIssue.relative
649
+ });
650
+ }
651
+ }
652
+
653
+ const success = failedCount === 0;
654
+
655
+ this.logger.info(`Repair complete: ${succeededCount}/${totalOperations} succeeded`);
656
+
657
+ return {
658
+ success,
659
+ results,
660
+ stats: {
661
+ total: totalOperations,
662
+ succeeded: succeededCount,
663
+ failed: failedCount,
664
+ byCategory: {
665
+ missing: {
666
+ succeeded: results.missing.filter(r => r.success).length,
667
+ failed: results.missing.filter(r => !r.success).length
668
+ },
669
+ corrupted: {
670
+ succeeded: results.corrupted.filter(r => r.success).length,
671
+ failed: results.corrupted.filter(r => !r.success).length
672
+ },
673
+ paths: {
674
+ succeeded: results.paths.filter(r => r.success).length,
675
+ failed: results.paths.filter(r => !r.success).length
676
+ }
677
+ }
678
+ }
679
+ };
680
+ }
681
+
682
+ /**
683
+ * Repairs a missing file or directory.
684
+ *
685
+ * For directories, recreates the entire directory structure from source.
686
+ * For files, copies from the package source.
687
+ *
688
+ * @param {Object} missingFile - Missing file descriptor
689
+ * @param {string} missingFile.path - Absolute path to the missing file/directory
690
+ * @param {string} missingFile.type - 'directory' or 'file'
691
+ * @param {string} missingFile.name - Display name for logging
692
+ * @returns {Promise<void>}
693
+ * @private
694
+ */
695
+ async _repairMissingFile(missingFile) {
696
+ const targetDir = this.scopeManager.getTargetDir();
697
+
698
+ if (missingFile.type === 'directory') {
699
+ // Recreate directory from source
700
+ const dirName = path.basename(missingFile.path);
701
+ const sourceDir = this._getSourcePath(dirName);
702
+ const targetPath = path.join(targetDir, dirName);
703
+
704
+ // Use fileOps._copyFile for each file in the directory
705
+ await this._copyDirectory(sourceDir, targetPath);
706
+ } else {
707
+ // Recreate single file
708
+ const relativePath = path.relative(targetDir, missingFile.path);
709
+ const sourcePath = this._getSourcePath(relativePath);
710
+ await this.fileOps._copyFile(sourcePath, missingFile.path);
711
+ }
712
+ }
713
+
714
+ /**
715
+ * Recursively copies a directory.
716
+ *
717
+ * @param {string} sourceDir - Source directory
718
+ * @param {string} targetDir - Target directory
719
+ * @returns {Promise<void>}
720
+ * @private
721
+ */
722
+ async _copyDirectory(sourceDir, targetDir) {
723
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
724
+
725
+ for (const entry of entries) {
726
+ const sourcePath = path.join(sourceDir, entry.name);
727
+ const targetPath = path.join(targetDir, entry.name);
728
+
729
+ if (entry.isDirectory()) {
730
+ await fs.mkdir(targetPath, { recursive: true });
731
+ await this._copyDirectory(sourcePath, targetPath);
732
+ } else {
733
+ await this.fileOps._copyFile(sourcePath, targetPath);
734
+ }
735
+ }
736
+ }
737
+
738
+ /**
739
+ * Resolves source file path from package installation.
740
+ *
741
+ * Uses __dirname to find the source file in the package.
742
+ *
743
+ * @param {string} relativePath - Path relative to installation root
744
+ * @returns {string} Absolute path to source file
745
+ * @throws {Error} If source file doesn't exist
746
+ * @private
747
+ */
748
+ _getSourcePath(relativePath) {
749
+ // Resolve from the package root (parent of src/services)
750
+ const packageRoot = path.resolve(__dirname, '../..');
751
+ const sourcePath = path.join(packageRoot, relativePath);
752
+
753
+ return sourcePath;
754
+ }
755
+
756
+ /**
757
+ * Generates a human-readable summary of issues.
758
+ *
759
+ * @param {Object} issues - Issues object from detectIssues()
760
+ * @returns {string} Formatted summary string
761
+ *
762
+ * @example
763
+ * const summary = repairService.generateSummary(issues);
764
+ * console.log(summary);
765
+ * // Missing Files (2):
766
+ * // - agents directory
767
+ * // - command/gsd/help.md
768
+ * //
769
+ * // Corrupted Files (1):
770
+ * // - agents/ro-commit.md
771
+ */
772
+ generateSummary(issues) {
773
+ const lines = [];
774
+
775
+ // Missing Files
776
+ if (issues.missingFiles && issues.missingFiles.length > 0) {
777
+ lines.push(`Missing Files (${issues.missingFiles.length}):`);
778
+ for (const file of issues.missingFiles) {
779
+ lines.push(` - ${file.name}`);
780
+ }
781
+ lines.push('');
782
+ }
783
+
784
+ // Corrupted Files
785
+ if (issues.corruptedFiles && issues.corruptedFiles.length > 0) {
786
+ lines.push(`Corrupted Files (${issues.corruptedFiles.length}):`);
787
+ for (const file of issues.corruptedFiles) {
788
+ lines.push(` - ${file.relative}`);
789
+ }
790
+ lines.push('');
791
+ }
792
+
793
+ // Path Issues
794
+ if (issues.pathIssues && issues.pathIssues.length > 0) {
795
+ lines.push(`Path Issues (${issues.pathIssues.length}):`);
796
+ for (const file of issues.pathIssues) {
797
+ lines.push(` - ${file.relative}`);
798
+ }
799
+ lines.push('');
800
+ }
801
+
802
+ return lines.join('\n').trim();
803
+ }
804
+
805
+ /**
806
+ * Validates the repair results structure.
807
+ *
808
+ * @param {Object} results - Repair results from repair()
809
+ * @returns {boolean} True if results structure is valid
810
+ * @private
811
+ */
812
+ _validateRepairResults(results) {
813
+ if (!results || typeof results !== 'object') {
814
+ this.logger.warning('Invalid repair results: not an object');
815
+ return false;
816
+ }
817
+
818
+ if (typeof results.success !== 'boolean') {
819
+ this.logger.warning('Invalid repair results: missing success boolean');
820
+ return false;
821
+ }
822
+
823
+ if (!results.results || typeof results.results !== 'object') {
824
+ this.logger.warning('Invalid repair results: missing results object');
825
+ return false;
826
+ }
827
+
828
+ if (!results.stats || typeof results.stats !== 'object') {
829
+ this.logger.warning('Invalid repair results: missing stats object');
830
+ return false;
831
+ }
832
+
833
+ return true;
834
+ }
835
+ }
836
+
837
+ /**
838
+ * Default export for the repair-service module.
839
+ *
840
+ * @example
841
+ * import { RepairService } from './services/repair-service.js';
842
+ * const repairService = new RepairService({ scopeManager, backupManager, fileOps, logger, expectedVersion });
843
+ */
844
+ export default {
845
+ RepairService
846
+ };