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.
- package/agents/gsd-debugger.md +5 -5
- package/agents/gsd-settings.md +476 -30
- package/bin/gsd-install.js +105 -0
- package/bin/gsd.js +352 -0
- package/{command → commands}/gsd/add-phase.md +1 -1
- package/{command → commands}/gsd/audit-milestone.md +1 -1
- package/{command → commands}/gsd/debug.md +3 -3
- package/{command → commands}/gsd/discuss-phase.md +1 -1
- package/{command → commands}/gsd/execute-phase.md +1 -1
- package/{command → commands}/gsd/list-phase-assumptions.md +1 -1
- package/{command → commands}/gsd/map-codebase.md +1 -1
- package/{command → commands}/gsd/new-milestone.md +1 -1
- package/{command → commands}/gsd/new-project.md +3 -3
- package/{command → commands}/gsd/plan-phase.md +2 -2
- package/{command → commands}/gsd/research-phase.md +1 -1
- package/{command → commands}/gsd/verify-work.md +1 -1
- package/get-shit-done/workflows/list-phase-assumptions.md +1 -1
- package/get-shit-done/workflows/verify-work.md +5 -5
- package/lib/constants.js +199 -0
- package/package.json +34 -20
- package/src/commands/check.js +329 -0
- package/src/commands/config.js +337 -0
- package/src/commands/install.js +608 -0
- package/src/commands/list.js +256 -0
- package/src/commands/repair.js +519 -0
- package/src/commands/uninstall.js +732 -0
- package/src/commands/update.js +444 -0
- package/src/services/backup-manager.js +585 -0
- package/src/services/config.js +262 -0
- package/src/services/file-ops.js +855 -0
- package/src/services/health-checker.js +475 -0
- package/src/services/manifest-manager.js +301 -0
- package/src/services/migration-service.js +831 -0
- package/src/services/repair-service.js +846 -0
- package/src/services/scope-manager.js +303 -0
- package/src/services/settings.js +553 -0
- package/src/services/structure-detector.js +240 -0
- package/src/services/update-service.js +863 -0
- package/src/utils/hash.js +71 -0
- package/src/utils/interactive.js +222 -0
- package/src/utils/logger.js +128 -0
- package/src/utils/npm-registry.js +255 -0
- package/src/utils/path-resolver.js +226 -0
- /package/{command → commands}/gsd/add-todo.md +0 -0
- /package/{command → commands}/gsd/check-todos.md +0 -0
- /package/{command → commands}/gsd/complete-milestone.md +0 -0
- /package/{command → commands}/gsd/help.md +0 -0
- /package/{command → commands}/gsd/insert-phase.md +0 -0
- /package/{command → commands}/gsd/pause-work.md +0 -0
- /package/{command → commands}/gsd/plan-milestone-gaps.md +0 -0
- /package/{command → commands}/gsd/progress.md +0 -0
- /package/{command → commands}/gsd/quick.md +0 -0
- /package/{command → commands}/gsd/remove-phase.md +0 -0
- /package/{command → commands}/gsd/resume-work.md +0 -0
- /package/{command → commands}/gsd/set-model.md +0 -0
- /package/{command → commands}/gsd/set-profile.md +0 -0
- /package/{command → commands}/gsd/settings.md +0 -0
- /package/{command → commands}/gsd/update.md +0 -0
- /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
|
+
};
|