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,475 @@
1
+ /**
2
+ * Health checker service for verifying installation integrity.
3
+ *
4
+ * This module provides comprehensive health checking capabilities for
5
+ * GSD-OpenCode installations. It can verify file existence, version matching,
6
+ * and file integrity through hash comparison. Works in conjunction with
7
+ * ScopeManager to handle both global and local installations.
8
+ *
9
+ * @module health-checker
10
+ */
11
+
12
+ import fs from 'fs/promises';
13
+ import path from 'path';
14
+ import { ScopeManager } from './scope-manager.js';
15
+ import { hashFile } from '../utils/hash.js';
16
+ import { DIRECTORIES_TO_COPY, VERSION_FILE } from '../../lib/constants.js';
17
+ import { StructureDetector, STRUCTURE_TYPES } from './structure-detector.js';
18
+
19
+ /**
20
+ * Manages health verification for GSD-OpenCode installations.
21
+ *
22
+ * This class provides methods to verify installation integrity through
23
+ * multiple check categories: file existence, version matching, and file
24
+ * integrity (hash-based). It uses ScopeManager for path resolution and
25
+ * follows the established service layer pattern.
26
+ *
27
+ * @class HealthChecker
28
+ * @example
29
+ * const scope = new ScopeManager({ scope: 'global' });
30
+ * const health = new HealthChecker(scope);
31
+ *
32
+ * // Check all aspects
33
+ * const result = await health.checkAll({ expectedVersion: '1.0.0' });
34
+ * if (result.passed) {
35
+ * console.log('Installation is healthy');
36
+ * } else {
37
+ * console.log(`Issues found: ${result.categories.files.checks.filter(c => !c.passed).length} files`);
38
+ * }
39
+ */
40
+ export class HealthChecker {
41
+ /**
42
+ * Creates a new HealthChecker instance.
43
+ *
44
+ * @param {ScopeManager} scopeManager - ScopeManager instance for path resolution
45
+ * @throws {Error} If scopeManager is not provided or invalid
46
+ *
47
+ * @example
48
+ * const scope = new ScopeManager({ scope: 'global' });
49
+ * const health = new HealthChecker(scope);
50
+ */
51
+ constructor(scopeManager) {
52
+ if (!scopeManager) {
53
+ throw new Error('ScopeManager instance is required');
54
+ }
55
+
56
+ if (typeof scopeManager.getTargetDir !== 'function') {
57
+ throw new Error('Invalid ScopeManager: missing getTargetDir method');
58
+ }
59
+
60
+ this.scopeManager = scopeManager;
61
+ this.targetDir = scopeManager.getTargetDir();
62
+ this.structureDetector = new StructureDetector(this.targetDir);
63
+ }
64
+
65
+ /**
66
+ * Detects the directory structure and returns status information.
67
+ *
68
+ * Uses StructureDetector to determine if the installation uses the old
69
+ * (command/gsd/), new (commands/gsd/), dual (both), or no structure.
70
+ *
71
+ * @returns {Promise<Object>} Structure detection results
72
+ * @property {string} type - One of STRUCTURE_TYPES (old, new, dual, none)
73
+ * @property {string} label - Human-readable label for the structure
74
+ * @property {boolean} needsMigration - True if migration is recommended
75
+ * @property {boolean} isHealthy - True if structure is valid (new or none)
76
+ *
77
+ * @example
78
+ * const structure = await health.detectStructure();
79
+ * if (structure.needsMigration) {
80
+ * console.log(`Migration needed: ${structure.label}`);
81
+ * }
82
+ */
83
+ async detectStructure() {
84
+ const structure = await this.structureDetector.detect();
85
+
86
+ const status = {
87
+ type: structure,
88
+ label: this._getStructureLabel(structure),
89
+ needsMigration: structure === STRUCTURE_TYPES.OLD || structure === STRUCTURE_TYPES.DUAL,
90
+ isHealthy: structure === STRUCTURE_TYPES.NEW || structure === STRUCTURE_TYPES.NONE
91
+ };
92
+
93
+ return status;
94
+ }
95
+
96
+ /**
97
+ * Gets a human-readable label for a structure type.
98
+ *
99
+ * @private
100
+ * @param {string} type - One of STRUCTURE_TYPES values
101
+ * @returns {string} Human-readable label
102
+ */
103
+ _getStructureLabel(type) {
104
+ const labels = {
105
+ [STRUCTURE_TYPES.OLD]: 'Legacy (command/gsd/)',
106
+ [STRUCTURE_TYPES.NEW]: 'Current (commands/gsd/)',
107
+ [STRUCTURE_TYPES.DUAL]: 'Dual (both structures)',
108
+ [STRUCTURE_TYPES.NONE]: 'No command structure'
109
+ };
110
+ return labels[type] || 'Unknown';
111
+ }
112
+
113
+ /**
114
+ * Verifies that all required files and directories exist.
115
+ *
116
+ * Checks each directory in DIRECTORIES_TO_COPY and the VERSION file.
117
+ * Returns structured results suitable for CLI output.
118
+ *
119
+ * @returns {Promise<Object>} File verification results
120
+ * @property {boolean} passed - True if all required files exist
121
+ * @property {Array} checks - Detailed check results for each file/directory
122
+ *
123
+ * @example
124
+ * const result = await health.verifyFiles();
125
+ * console.log(result.passed); // true/false
126
+ * console.log(result.checks);
127
+ * // [
128
+ * // { name: 'agents directory', passed: true, path: '/.../agents' },
129
+ * // { name: 'VERSION file', passed: true, path: '/.../VERSION' }
130
+ * // ]
131
+ */
132
+ async verifyFiles() {
133
+ const checks = [];
134
+ let allPassed = true;
135
+
136
+ // Check each required directory
137
+ for (const dirName of DIRECTORIES_TO_COPY) {
138
+ const dirPath = path.join(this.targetDir, dirName);
139
+ try {
140
+ const stats = await fs.stat(dirPath);
141
+ const passed = stats.isDirectory();
142
+ checks.push({
143
+ name: `${dirName} directory`,
144
+ passed,
145
+ path: dirPath
146
+ });
147
+ if (!passed) allPassed = false;
148
+ } catch (error) {
149
+ checks.push({
150
+ name: `${dirName} directory`,
151
+ passed: false,
152
+ path: dirPath,
153
+ error: error.code === 'ENOENT' ? 'Directory not found' : error.message
154
+ });
155
+ allPassed = false;
156
+ }
157
+ }
158
+
159
+ // Check VERSION file
160
+ const versionPath = path.join(this.targetDir, VERSION_FILE);
161
+ try {
162
+ const stats = await fs.stat(versionPath);
163
+ const passed = stats.isFile();
164
+ checks.push({
165
+ name: 'VERSION file',
166
+ passed,
167
+ path: versionPath
168
+ });
169
+ if (!passed) allPassed = false;
170
+ } catch (error) {
171
+ checks.push({
172
+ name: 'VERSION file',
173
+ passed: false,
174
+ path: versionPath,
175
+ error: error.code === 'ENOENT' ? 'File not found' : error.message
176
+ });
177
+ allPassed = false;
178
+ }
179
+
180
+ return {
181
+ passed: allPassed,
182
+ checks
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Verifies that the installed version matches the expected version.
188
+ *
189
+ * Reads the VERSION file and compares its content with the expected
190
+ * version string. Handles cases where VERSION file doesn't exist.
191
+ *
192
+ * @param {string} expectedVersion - The expected version string (e.g., '1.0.0')
193
+ * @returns {Promise<Object>} Version verification results
194
+ * @property {boolean} passed - True if version matches
195
+ * @property {string|null} installed - The installed version, or null if not found
196
+ * @property {string} expected - The expected version that was checked
197
+ * @property {Array} checks - Detailed check results
198
+ *
199
+ * @example
200
+ * const result = await health.verifyVersion('1.0.0');
201
+ * console.log(result.passed); // true if VERSION contains '1.0.0'
202
+ * console.log(result.installed); // '1.0.0' or null
203
+ */
204
+ async verifyVersion(expectedVersion) {
205
+ if (!expectedVersion || typeof expectedVersion !== 'string') {
206
+ throw new Error('Expected version must be a non-empty string');
207
+ }
208
+
209
+ const versionPath = path.join(this.targetDir, VERSION_FILE);
210
+ let installedVersion = null;
211
+ let passed = false;
212
+ let error = null;
213
+
214
+ try {
215
+ const content = await fs.readFile(versionPath, 'utf-8');
216
+ installedVersion = content.trim();
217
+ passed = installedVersion === expectedVersion;
218
+ } catch (err) {
219
+ if (err.code === 'ENOENT') {
220
+ error = 'VERSION file not found';
221
+ } else if (err.code === 'EACCES') {
222
+ error = 'Permission denied reading VERSION file';
223
+ } else {
224
+ error = err.message;
225
+ }
226
+ }
227
+
228
+ return {
229
+ passed,
230
+ installed: installedVersion,
231
+ expected: expectedVersion,
232
+ checks: [{
233
+ name: 'version match',
234
+ passed,
235
+ installed: installedVersion,
236
+ expected: expectedVersion,
237
+ error
238
+ }]
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Verifies file integrity by checking that key files are readable.
244
+ *
245
+ * For v1, this performs basic integrity checks by verifying that
246
+ * sample files from each required directory exist and are readable.
247
+ * Future versions may compare against known-good hashes.
248
+ *
249
+ * @returns {Promise<Object>} Integrity verification results
250
+ * @property {boolean} passed - True if all integrity checks pass
251
+ * @property {Array} checks - Detailed check results for each file
252
+ *
253
+ * @example
254
+ * const result = await health.verifyIntegrity();
255
+ * console.log(result.passed); // true/false
256
+ * console.log(result.checks);
257
+ * // [
258
+ * // { file: '/.../agents/README.md', passed: true },
259
+ * // { file: '/.../command/gsd/help.md', passed: true }
260
+ * // ]
261
+ */
262
+ async verifyIntegrity() {
263
+ const checks = [];
264
+ let allPassed = true;
265
+
266
+ // Check sample files from each required directory
267
+ // These represent key files that should always exist
268
+ const sampleFiles = [
269
+ { dir: 'agents', file: 'gsd-executor.md' },
270
+ { dir: 'command', file: 'gsd/help.md' },
271
+ { dir: 'get-shit-done', file: 'templates/summary.md' }
272
+ ];
273
+
274
+ for (const { dir, file } of sampleFiles) {
275
+ const filePath = path.join(this.targetDir, dir, file);
276
+ try {
277
+ // Try to read and hash the file
278
+ const hash = await hashFile(filePath);
279
+ const passed = hash !== null;
280
+ checks.push({
281
+ file: filePath,
282
+ hash,
283
+ passed,
284
+ relative: path.join(dir, file)
285
+ });
286
+ if (!passed) allPassed = false;
287
+ } catch (error) {
288
+ checks.push({
289
+ file: filePath,
290
+ hash: null,
291
+ passed: false,
292
+ relative: path.join(dir, file),
293
+ error: error.code === 'ENOENT' ? 'File not found' : error.message
294
+ });
295
+ allPassed = false;
296
+ }
297
+ }
298
+
299
+ // Also verify VERSION file is readable (counts as integrity check)
300
+ const versionPath = path.join(this.targetDir, VERSION_FILE);
301
+ try {
302
+ const content = await fs.readFile(versionPath, 'utf-8');
303
+ checks.push({
304
+ file: versionPath,
305
+ hash: null, // We don't hash VERSION file
306
+ passed: true,
307
+ relative: VERSION_FILE
308
+ });
309
+ } catch (error) {
310
+ checks.push({
311
+ file: versionPath,
312
+ hash: null,
313
+ passed: false,
314
+ relative: VERSION_FILE,
315
+ error: error.code === 'ENOENT' ? 'File not found' : error.message
316
+ });
317
+ allPassed = false;
318
+ }
319
+
320
+ return {
321
+ passed: allPassed,
322
+ checks
323
+ };
324
+ }
325
+
326
+ /**
327
+ * Checks if a structure type can be repaired.
328
+ *
329
+ * Determines if the given structure type can be automatically repaired
330
+ * (migrated from old to new structure).
331
+ *
332
+ * @param {string} structureType - One of STRUCTURE_TYPES values
333
+ * @returns {boolean} True if structure can be repaired
334
+ *
335
+ * @example
336
+ * const canRepair = healthChecker.canRepairStructure(STRUCTURE_TYPES.OLD);
337
+ * // Returns true
338
+ */
339
+ canRepairStructure(structureType) {
340
+ return structureType === STRUCTURE_TYPES.OLD ||
341
+ structureType === STRUCTURE_TYPES.DUAL;
342
+ }
343
+
344
+ /**
345
+ * Gets repair recommendation for structure issues.
346
+ *
347
+ * Returns appropriate repair command and message based on structure state.
348
+ *
349
+ * @param {string} structureType - One of STRUCTURE_TYPES values
350
+ * @returns {Object} Repair recommendation
351
+ * @property {boolean} canRepair - True if repair is possible
352
+ * @property {string|null} command - Repair command to run
353
+ * @property {string} message - Human-readable recommendation
354
+ *
355
+ * @example
356
+ * const recommendation = healthChecker.getStructureRecommendation(STRUCTURE_TYPES.DUAL);
357
+ * console.log(recommendation.command); // 'gsd-opencode repair --fix-structure'
358
+ */
359
+ getStructureRecommendation(structureType) {
360
+ const canRepair = this.canRepairStructure(structureType);
361
+
362
+ if (!canRepair) {
363
+ return {
364
+ canRepair: false,
365
+ command: null,
366
+ message: structureType === STRUCTURE_TYPES.NEW
367
+ ? 'Structure is up to date'
368
+ : 'No structure detected'
369
+ };
370
+ }
371
+
372
+ const isDual = structureType === STRUCTURE_TYPES.DUAL;
373
+ return {
374
+ canRepair: true,
375
+ command: 'gsd-opencode repair --fix-structure',
376
+ message: isDual
377
+ ? 'Dual structure detected (both old and new exist). Run repair --fix-structure to consolidate.'
378
+ : 'Old structure detected (command/gsd/). Run repair --fix-structure to migrate.'
379
+ };
380
+ }
381
+
382
+ /**
383
+ * Runs all health checks and returns aggregated results.
384
+ *
385
+ * This is the main entry point for health verification. It runs
386
+ * file existence, version matching, and integrity checks, then
387
+ * aggregates the results with an overall pass/fail status and
388
+ * suggested exit code.
389
+ *
390
+ * @param {Object} options - Check options
391
+ * @param {string} [options.expectedVersion] - Expected version for version check
392
+ * @param {boolean} [options.verbose=false] - Include verbose output
393
+ * @returns {Promise<Object>} Complete health check results
394
+ * @property {boolean} passed - True if all checks pass
395
+ * @property {number} exitCode - Suggested exit code (0 for healthy, 1 for issues)
396
+ * @property {Object} categories - Results from each check category
397
+ * @property {Object} categories.files - File existence check results
398
+ * @property {Object} categories.version - Version match check results
399
+ * @property {Object} categories.integrity - Integrity check results
400
+ * @property {Object} categories.structure - Structure check results with repair info
401
+ *
402
+ * @example
403
+ * const result = await health.checkAll({ expectedVersion: '1.0.0' });
404
+ * console.log(result.passed); // true/false
405
+ * console.log(result.exitCode); // 0 or 1
406
+ * console.log(result.categories.files.passed); // etc.
407
+ * console.log(result.categories.structure.repairCommand); // 'gsd-opencode repair --fix-structure'
408
+ */
409
+ async checkAll(options = {}) {
410
+ const { expectedVersion, verbose = false } = options;
411
+
412
+ // Run all checks in parallel including structure detection
413
+ const [filesResult, integrityResult, structureResult] = await Promise.all([
414
+ this.verifyFiles(),
415
+ this.verifyIntegrity(),
416
+ this.detectStructure()
417
+ ]);
418
+
419
+ // Version check only if expectedVersion provided
420
+ let versionResult = null;
421
+ if (expectedVersion) {
422
+ versionResult = await this.verifyVersion(expectedVersion);
423
+ }
424
+
425
+ // Determine overall status
426
+ // Dual structure is considered unhealthy (requires action)
427
+ const allResults = [filesResult.passed, integrityResult.passed];
428
+ if (versionResult) {
429
+ allResults.push(versionResult.passed);
430
+ }
431
+
432
+ // Structure check: NEW and NONE are healthy, OLD and DUAL need attention
433
+ // DUAL structure causes failure (non-zero exit code)
434
+ const structureHealthy = structureResult.type === STRUCTURE_TYPES.NEW ||
435
+ structureResult.type === STRUCTURE_TYPES.NONE;
436
+ if (!structureHealthy) {
437
+ allResults.push(false);
438
+ }
439
+
440
+ // Add repair information to structure result
441
+ const repairRecommendation = this.getStructureRecommendation(structureResult.type);
442
+ const enhancedStructureResult = {
443
+ ...structureResult,
444
+ canRepair: repairRecommendation.canRepair,
445
+ repairCommand: repairRecommendation.command,
446
+ repairMessage: repairRecommendation.message
447
+ };
448
+
449
+ const allPassed = allResults.every(r => r);
450
+
451
+ return {
452
+ passed: allPassed,
453
+ exitCode: allPassed ? 0 : 1,
454
+ categories: {
455
+ files: filesResult,
456
+ version: versionResult,
457
+ integrity: integrityResult,
458
+ structure: enhancedStructureResult
459
+ }
460
+ };
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Default export for the health-checker module.
466
+ *
467
+ * @example
468
+ * import { HealthChecker } from './services/health-checker.js';
469
+ * const scope = new ScopeManager({ scope: 'global' });
470
+ * const health = new HealthChecker(scope);
471
+ * const result = await health.checkAll({ expectedVersion: '1.0.0' });
472
+ */
473
+ export default {
474
+ HealthChecker
475
+ };