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,519 @@
1
+ /**
2
+ * Repair command for GSD-OpenCode CLI.
3
+ *
4
+ * This module provides the repair functionality to detect and fix broken
5
+ * installations, showing a summary of issues before fixing, requiring
6
+ * interactive confirmation, and displaying detailed post-repair reports.
7
+ *
8
+ * Implements requirements:
9
+ * - CLI-04: User can run gsd-opencode repair to fix broken installations
10
+ * - REPAIR-01: Repair detects and reinstalls missing files
11
+ * - REPAIR-02: Repair detects and replaces corrupted files
12
+ * - REPAIR-03: Repair fixes broken path references in .md files
13
+ * - REPAIR-04: Repair shows summary of issues found before fixing
14
+ * - REPAIR-05: Repair requires interactive confirmation before making changes
15
+ * - ERROR-01: All commands handle permission errors (EACCES) with exit code 2
16
+ * - ERROR-02: All commands handle signal interrupts (SIGINT/SIGTERM) gracefully
17
+ * - ERROR-03: All commands support --verbose flag for detailed debugging output
18
+ *
19
+ * @module commands/repair
20
+ * @description Repair command for fixing broken GSD-OpenCode installations
21
+ * @example
22
+ * // Repair with auto-detection
23
+ * await repairCommand({});
24
+ *
25
+ * // Repair global installation
26
+ * await repairCommand({ global: true });
27
+ *
28
+ * // Repair with verbose output
29
+ * await repairCommand({ verbose: true });
30
+ */
31
+
32
+ import { ScopeManager } from '../services/scope-manager.js';
33
+ import { BackupManager } from '../services/backup-manager.js';
34
+ import { FileOperations } from '../services/file-ops.js';
35
+ import { RepairService } from '../services/repair-service.js';
36
+ import { STRUCTURE_TYPES } from '../services/structure-detector.js';
37
+ import { promptConfirmation } from '../utils/interactive.js';
38
+ import { logger, setVerbose } from '../utils/logger.js';
39
+ import { ERROR_CODES } from '../../lib/constants.js';
40
+ import fs from 'fs/promises';
41
+ import path from 'path';
42
+ import { fileURLToPath } from 'url';
43
+ import ora from 'ora';
44
+
45
+ /**
46
+ * Gets the package version from package.json.
47
+ *
48
+ * @returns {Promise<string>} The package version
49
+ * @private
50
+ */
51
+ async function getPackageVersion() {
52
+ try {
53
+ const __filename = fileURLToPath(import.meta.url);
54
+ const __dirname = path.dirname(__filename);
55
+ const packageRoot = path.resolve(__dirname, '../..');
56
+ const packageJsonPath = path.join(packageRoot, 'package.json');
57
+
58
+ const content = await fs.readFile(packageJsonPath, 'utf-8');
59
+ const pkg = JSON.parse(content);
60
+ return pkg.version || '1.0.0';
61
+ } catch (error) {
62
+ logger.warning('Could not read package version, using 1.0.0');
63
+ return '1.0.0';
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Displays a formatted summary of detected issues.
69
+ *
70
+ * Groups issues by category: Missing Files, Corrupted Files, Path Issues.
71
+ * Shows count and lists files for each category.
72
+ *
73
+ * @param {Object} issues - Issues object from detectIssues()
74
+ * @param {string} scopeLabel - Label for this scope (Global/Local)
75
+ * @private
76
+ */
77
+ function displayIssuesSummary(issues, scopeLabel) {
78
+ logger.heading(`${scopeLabel} Installation Repair`);
79
+ logger.dim('================================');
80
+ logger.dim('');
81
+
82
+ // Missing Files
83
+ if (issues.missingFiles && issues.missingFiles.length > 0) {
84
+ logger.info(`Missing Files (${issues.missingFiles.length}):`);
85
+ for (const file of issues.missingFiles) {
86
+ logger.dim(` ✗ ${file.name}`);
87
+ }
88
+ logger.dim('');
89
+ }
90
+
91
+ // Corrupted Files
92
+ if (issues.corruptedFiles && issues.corruptedFiles.length > 0) {
93
+ logger.info(`Corrupted Files (${issues.corruptedFiles.length}):`);
94
+ for (const file of issues.corruptedFiles) {
95
+ logger.dim(` ✗ ${file.relative}`);
96
+ }
97
+ logger.dim('');
98
+ }
99
+
100
+ // Path Issues
101
+ if (issues.pathIssues && issues.pathIssues.length > 0) {
102
+ logger.info(`Path Issues (${issues.pathIssues.length}):`);
103
+ for (const file of issues.pathIssues) {
104
+ logger.dim(` ✗ ${file.relative}`);
105
+ }
106
+ logger.dim('');
107
+ }
108
+
109
+ // Total
110
+ logger.info(`Total issues: ${issues.totalIssues}`);
111
+ logger.dim('');
112
+ }
113
+
114
+ /**
115
+ * Displays detailed post-repair results.
116
+ *
117
+ * Shows success/failure status for each repaired file grouped by category.
118
+ *
119
+ * @param {Object} results - Repair results from repairService.repair()
120
+ * @param {string} scopeLabel - Label for this scope (Global/Local)
121
+ * @private
122
+ */
123
+ function displayRepairResults(results, scopeLabel) {
124
+ logger.heading(`Repair Results for ${scopeLabel} Installation`);
125
+ logger.dim('=====================================');
126
+ logger.dim('');
127
+
128
+ // Missing Files results
129
+ if (results.results.missing && results.results.missing.length > 0) {
130
+ const succeeded = results.results.missing.filter(r => r.success).length;
131
+ const failed = results.results.missing.filter(r => !r.success).length;
132
+ const status = failed === 0 ? `${succeeded} fixed` : `${succeeded} fixed, ${failed} failed`;
133
+ logger.info(`Missing Files: ${status}`);
134
+
135
+ for (const result of results.results.missing) {
136
+ if (result.success) {
137
+ logger.success(` ✓ ${path.basename(result.file)}`);
138
+ } else {
139
+ logger.error(` ✗ ${path.basename(result.file)} (${result.error})`);
140
+ }
141
+ }
142
+ logger.dim('');
143
+ }
144
+
145
+ // Corrupted Files results
146
+ if (results.results.corrupted && results.results.corrupted.length > 0) {
147
+ const succeeded = results.results.corrupted.filter(r => r.success).length;
148
+ const failed = results.results.corrupted.filter(r => !r.success).length;
149
+ const status = failed === 0 ? `${succeeded} fixed` : `${succeeded} fixed, ${failed} failed`;
150
+ logger.info(`Corrupted Files: ${status}`);
151
+
152
+ for (const result of results.results.corrupted) {
153
+ const fileName = path.basename(result.file);
154
+ if (result.success) {
155
+ logger.success(` ✓ ${fileName}`);
156
+ } else {
157
+ logger.error(` ✗ ${fileName} (${result.error})`);
158
+ }
159
+ }
160
+ logger.dim('');
161
+ }
162
+
163
+ // Path Issues results
164
+ if (results.results.paths && results.results.paths.length > 0) {
165
+ const succeeded = results.results.paths.filter(r => r.success).length;
166
+ const failed = results.results.paths.filter(r => !r.success).length;
167
+ const status = failed === 0 ? `${succeeded} fixed` : `${succeeded} fixed, ${failed} failed`;
168
+ logger.info(`Path Issues: ${status}`);
169
+
170
+ for (const result of results.results.paths) {
171
+ const fileName = path.basename(result.file);
172
+ if (result.success) {
173
+ logger.success(` ✓ ${fileName}`);
174
+ } else {
175
+ logger.error(` ✗ ${fileName} (${result.error})`);
176
+ }
177
+ }
178
+ logger.dim('');
179
+ }
180
+
181
+ // Summary
182
+ const totalSucceeded = results.stats.succeeded;
183
+ const totalFailed = results.stats.failed;
184
+ logger.info(`Summary: ${totalSucceeded} succeeded, ${totalFailed} failed`);
185
+ logger.dim('');
186
+ }
187
+
188
+ /**
189
+ * Checks a single scope for issues and returns results.
190
+ *
191
+ * @param {string} scope - 'global' or 'local'
192
+ * @param {Object} options - Options
193
+ * @param {boolean} options.verbose - Enable verbose output
194
+ * @param {boolean} options.fixStructure - Fix structure issues
195
+ * @param {boolean} options.fixAll - Fix all issues including structure
196
+ * @returns {Promise<Object>} Check results with installed flag, issues, and scopeLabel
197
+ * @private
198
+ */
199
+ async function checkScope(scope, options = {}) {
200
+ const scopeManager = new ScopeManager({ scope });
201
+ const scopeLabel = scope.charAt(0).toUpperCase() + scope.slice(1);
202
+
203
+ logger.debug(`Checking ${scope} installation for issues...`);
204
+
205
+ const isInstalled = await scopeManager.isInstalled();
206
+ if (!isInstalled) {
207
+ logger.debug(`No ${scope} installation found`);
208
+ return {
209
+ installed: false,
210
+ scope,
211
+ scopeLabel,
212
+ issues: null
213
+ };
214
+ }
215
+
216
+ logger.debug(`${scope} installation detected, detecting issues...`);
217
+
218
+ const backupManager = new BackupManager(scopeManager, logger);
219
+ const fileOps = new FileOperations(scopeManager, logger);
220
+ const expectedVersion = await getPackageVersion();
221
+
222
+ const repairService = new RepairService({
223
+ scopeManager,
224
+ backupManager,
225
+ fileOps,
226
+ logger,
227
+ expectedVersion
228
+ });
229
+
230
+ try {
231
+ // Check structure first
232
+ const structureCheck = await repairService.checkStructure();
233
+
234
+ // Handle structure issues if requested
235
+ if ((options.fixStructure || options.fixAll) && structureCheck.canRepair) {
236
+ logger.info(`Repairing ${scope} structure (${structureCheck.type})...`);
237
+
238
+ const structureResult = structureCheck.type === STRUCTURE_TYPES.DUAL
239
+ ? await repairService.fixDualStructure()
240
+ : await repairService.repairStructure();
241
+
242
+ if (structureResult.repaired || structureResult.fixed) {
243
+ logger.success(`Structure repaired: ${structureResult.message}`);
244
+ if (structureResult.backup) {
245
+ logger.dim(`Backup created: ${structureResult.backup}`);
246
+ }
247
+ } else {
248
+ logger.warning(`Structure repair: ${structureResult.message}`);
249
+ }
250
+
251
+ logger.dim('');
252
+ }
253
+
254
+ // Detect file issues
255
+ const issues = await repairService.detectIssues();
256
+
257
+ return {
258
+ installed: true,
259
+ scope,
260
+ scopeLabel,
261
+ issues,
262
+ repairService,
263
+ backupManager,
264
+ structureCheck
265
+ };
266
+ } catch (error) {
267
+ logger.debug(`Error during issue detection: ${error.message}`);
268
+ return {
269
+ installed: true,
270
+ scope,
271
+ scopeLabel,
272
+ issues: null,
273
+ error: error.message
274
+ };
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Main repair command function.
280
+ *
281
+ * Orchestrates the repair process:
282
+ * 1. Parse options and set verbose mode
283
+ * 2. Determine scopes to check (global, local, or both)
284
+ * 3. For each scope:
285
+ * - Detect installation issues using RepairService
286
+ * - Check for structure issues (dual/old structure)
287
+ * - Repair structure if --fix-structure or --fix-all flag provided
288
+ * - Display summary of issues found
289
+ * - Prompt for user confirmation
290
+ * - Perform repairs with progress indication
291
+ * - Display post-repair results
292
+ * 4. Return appropriate exit code
293
+ *
294
+ * @param {Object} options - Command options
295
+ * @param {boolean} [options.global] - Repair global installation only
296
+ * @param {boolean} [options.local] - Repair local installation only
297
+ * @param {boolean} [options.verbose] - Enable verbose output for debugging
298
+ * @param {boolean} [options.fixStructure] - Fix structure issues (migrate old to new)
299
+ * @param {boolean} [options.fixAll] - Fix all issues including structure
300
+ * @returns {Promise<number>} Exit code (0=success, 1=error, 2=permission, 130=interrupted)
301
+ * @async
302
+ *
303
+ * @example
304
+ * // Repair with auto-detection
305
+ * const exitCode = await repairCommand({});
306
+ *
307
+ * // Repair global installation
308
+ * const exitCode = await repairCommand({ global: true });
309
+ *
310
+ * // Repair with verbose output
311
+ * const exitCode = await repairCommand({ verbose: true });
312
+ *
313
+ * // Fix structure issues only
314
+ * const exitCode = await repairCommand({ fixStructure: true });
315
+ *
316
+ * // Fix all issues including structure
317
+ * const exitCode = await repairCommand({ fixAll: true });
318
+ */
319
+ export async function repairCommand(options = {}) {
320
+ const verbose = options.verbose || false;
321
+ const fixStructure = options.fixStructure || false;
322
+ const fixAll = options.fixAll || false;
323
+ setVerbose(verbose);
324
+
325
+ logger.debug('Starting repair command');
326
+ logger.debug(`Options: global=${options.global}, local=${options.local}, verbose=${verbose}, fixStructure=${fixStructure}, fixAll=${fixAll}`);
327
+
328
+ try {
329
+ logger.heading('GSD-OpenCode Installation Repair');
330
+ logger.dim('================================');
331
+ logger.dim('');
332
+
333
+ // Determine scopes to check
334
+ const scopesToCheck = [];
335
+ if (options.global) {
336
+ scopesToCheck.push('global');
337
+ } else if (options.local) {
338
+ scopesToCheck.push('local');
339
+ } else {
340
+ scopesToCheck.push('global', 'local');
341
+ }
342
+
343
+ let anyInstalled = false;
344
+ let anyRepaired = false;
345
+ let anyFailed = false;
346
+
347
+ for (const scope of scopesToCheck) {
348
+ try {
349
+ const result = await checkScope(scope, options);
350
+
351
+ if (!result.installed) {
352
+ logger.info(`No installation found at ${result.scopeLabel.toLowerCase()} scope`);
353
+ logger.dim('');
354
+ continue;
355
+ }
356
+
357
+ anyInstalled = true;
358
+
359
+ if (result.error) {
360
+ logger.error(`Failed to check ${scope} installation: ${result.error}`);
361
+ anyFailed = true;
362
+ continue;
363
+ }
364
+
365
+ // Check if there are structure issues to report
366
+ const hasStructureIssues = result.structureCheck &&
367
+ (result.structureCheck.type === STRUCTURE_TYPES.OLD ||
368
+ result.structureCheck.type === STRUCTURE_TYPES.DUAL);
369
+
370
+ // Handle structure-only repair mode
371
+ if (fixStructure || fixAll) {
372
+ // Structure was already repaired in checkScope
373
+ if (!result.issues.hasIssues && !hasStructureIssues) {
374
+ logger.success(`No issues detected at ${result.scopeLabel.toLowerCase()} scope`);
375
+ logger.dim('');
376
+ continue;
377
+ }
378
+ } else if (!result.issues.hasIssues) {
379
+ // No file issues - but check for structure issues to report
380
+ if (hasStructureIssues) {
381
+ logger.warning(`No file issues, but structure requires attention`);
382
+ logger.info(` Current structure: ${result.structureCheck.type}`);
383
+ logger.info(` Run with --fix-structure to repair`);
384
+ logger.dim('');
385
+ } else {
386
+ logger.success(`No issues detected at ${result.scopeLabel.toLowerCase()} scope`);
387
+ logger.dim('');
388
+ }
389
+ continue;
390
+ }
391
+
392
+ // Display issues summary
393
+ if (scopesToCheck.length > 1) {
394
+ logger.dim('');
395
+ }
396
+ displayIssuesSummary(result.issues, result.scopeLabel);
397
+
398
+ // Prompt for confirmation (repair always requires confirmation)
399
+ logger.debug('Requesting user confirmation...');
400
+ const confirmed = await promptConfirmation('Proceed with repairs?', false);
401
+
402
+ // Handle SIGINT (Ctrl+C) - user cancelled with interrupt
403
+ if (confirmed === null) {
404
+ logger.info('Repair cancelled');
405
+ return ERROR_CODES.INTERRUPTED;
406
+ }
407
+
408
+ // Handle explicit "no" response
409
+ if (!confirmed) {
410
+ logger.info('Repair cancelled');
411
+ return ERROR_CODES.SUCCESS;
412
+ }
413
+
414
+ logger.debug('User confirmed repair');
415
+
416
+ // Perform repairs with progress indication
417
+ const spinner = ora({
418
+ text: 'Starting repairs...',
419
+ spinner: 'dots',
420
+ color: 'cyan'
421
+ }).start();
422
+
423
+ const repairResult = await result.repairService.repair(result.issues, {
424
+ onProgress: ({ current, total }) => {
425
+ const percent = Math.round((current / total) * 100);
426
+ spinner.text = `Repairing ${current}/${total} files... (${percent}%)`;
427
+ }
428
+ });
429
+
430
+ if (repairResult.success) {
431
+ spinner.succeed('Repairs completed');
432
+ } else {
433
+ spinner.fail('Some repairs failed');
434
+ }
435
+
436
+ logger.dim('');
437
+
438
+ // Display post-repair results
439
+ displayRepairResults(repairResult, result.scopeLabel);
440
+
441
+ // Show backup location
442
+ const backupDir = result.backupManager.getBackupDir();
443
+ logger.dim(`Backups saved to: ${backupDir}`);
444
+ logger.dim('');
445
+
446
+ // Track overall status
447
+ if (repairResult.stats.succeeded > 0) {
448
+ anyRepaired = true;
449
+ }
450
+ if (repairResult.stats.failed > 0) {
451
+ anyFailed = true;
452
+ }
453
+
454
+ } catch (error) {
455
+ logger.error(`Failed to repair ${scope} installation: ${error.message}`);
456
+ anyFailed = true;
457
+
458
+ if (verbose) {
459
+ logger.debug(error.stack);
460
+ }
461
+ }
462
+ }
463
+
464
+ // Overall status message
465
+ logger.dim('');
466
+
467
+ if (!anyInstalled) {
468
+ logger.info('No GSD-OpenCode installation found to repair');
469
+ logger.dim('');
470
+ logger.info("Run 'gsd-opencode install' to install");
471
+ logger.dim('');
472
+ return ERROR_CODES.SUCCESS;
473
+ }
474
+
475
+ if (anyFailed) {
476
+ logger.error('Some repairs failed. Run gsd-opencode check for details.');
477
+ return ERROR_CODES.GENERAL_ERROR;
478
+ }
479
+
480
+ if (anyRepaired) {
481
+ logger.success('All repairs completed successfully');
482
+ }
483
+
484
+ return ERROR_CODES.SUCCESS;
485
+
486
+ } catch (error) {
487
+ // Handle Ctrl+C during async operations (AbortPromptError from @inquirer/prompts)
488
+ if (error.name === 'AbortPromptError' || error.message?.includes('cancel')) {
489
+ logger.info('Repair cancelled by user');
490
+ return ERROR_CODES.INTERRUPTED;
491
+ }
492
+
493
+ // Handle permission errors (EACCES)
494
+ if (error.code === 'EACCES') {
495
+ logger.error('Permission denied: Cannot access installation directory');
496
+ logger.dim('');
497
+ logger.dim('Suggestion: Check directory permissions or run with appropriate privileges');
498
+ return ERROR_CODES.PERMISSION_ERROR;
499
+ }
500
+
501
+ // Handle all other errors
502
+ logger.error(`Repair failed: ${error.message}`);
503
+
504
+ if (verbose && error.stack) {
505
+ logger.dim(error.stack);
506
+ }
507
+
508
+ return ERROR_CODES.GENERAL_ERROR;
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Default export for the repair command.
514
+ *
515
+ * @example
516
+ * import repairCommand from './commands/repair.js';
517
+ * const exitCode = await repairCommand({ global: true });
518
+ */
519
+ export default repairCommand;