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,444 @@
1
+ /**
2
+ * Update command for GSD-OpenCode CLI.
3
+ *
4
+ * This module provides the update functionality to check for and install
5
+ * new versions of GSD-OpenCode, showing version comparisons, requiring
6
+ * interactive confirmation, and displaying progress during installation.
7
+ *
8
+ * Implements requirements:
9
+ * - CLI-05: User can run gsd-opencode update to update to latest version
10
+ * - UPDATE-01: Update checks npm registry for latest version
11
+ * - UPDATE-02: Update supports --beta flag for private registry
12
+ * - UPDATE-03: Update supports specifying exact version to install
13
+ * - UPDATE-04: Update shows current and target versions before proceeding
14
+ * - UPDATE-05: Update preserves existing installation scope
15
+ * - UPDATE-06: Update performs full install procedure including path replacement
16
+ * - ERROR-01: All commands handle permission errors (EACCES) with exit code 2
17
+ * - ERROR-02: All commands handle signal interrupts (SIGINT/SIGTERM) gracefully
18
+ * - ERROR-03: All commands support --verbose flag for detailed debugging output
19
+ *
20
+ * @module commands/update
21
+ * @description Update command for managing GSD-OpenCode versions
22
+ * @example
23
+ * // Update to latest stable version
24
+ * await updateCommand({});
25
+ *
26
+ * // Update to latest beta
27
+ * await updateCommand({ beta: true });
28
+ *
29
+ * // Update to specific version
30
+ * await updateCommand({ version: '2.0.0' });
31
+ */
32
+
33
+ import { ScopeManager } from '../services/scope-manager.js';
34
+ import { BackupManager } from '../services/backup-manager.js';
35
+ import { FileOperations } from '../services/file-ops.js';
36
+ import { UpdateService } from '../services/update-service.js';
37
+ import { NpmRegistry } from '../utils/npm-registry.js';
38
+ import { promptConfirmation } from '../utils/interactive.js';
39
+ import { logger, setVerbose } from '../utils/logger.js';
40
+ import { ERROR_CODES } from '../../lib/constants.js';
41
+ import fs from 'fs/promises';
42
+ import path from 'path';
43
+ import { fileURLToPath } from 'url';
44
+ import ora from 'ora';
45
+
46
+ /**
47
+ * Gets the package version from package.json.
48
+ *
49
+ * @returns {Promise<string>} The package version
50
+ * @private
51
+ */
52
+ async function getPackageVersion() {
53
+ try {
54
+ const __filename = fileURLToPath(import.meta.url);
55
+ const __dirname = path.dirname(__filename);
56
+ const packageRoot = path.resolve(__dirname, '../..');
57
+ const packageJsonPath = path.join(packageRoot, 'package.json');
58
+
59
+ const content = await fs.readFile(packageJsonPath, 'utf-8');
60
+ const pkg = JSON.parse(content);
61
+ return pkg.version || '1.0.0';
62
+ } catch (error) {
63
+ logger.warning('Could not read package version, using 1.0.0');
64
+ return '1.0.0';
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Displays formatted update information.
70
+ *
71
+ * Shows current version, target version, and installation scope
72
+ * in a clear, formatted manner.
73
+ *
74
+ * @param {string|null} current - Current installed version
75
+ * @param {string} target - Target version to install
76
+ * @param {string} scopeLabel - Label for scope (Global/Local)
77
+ * @private
78
+ */
79
+ function displayUpdateInfo(current, target, scopeLabel) {
80
+ logger.heading(`${scopeLabel} Installation Update`);
81
+ logger.dim('========================');
82
+ logger.dim('');
83
+
84
+ logger.info(`Current version: ${current || 'Not installed'}`);
85
+ logger.info(`Target version: ${target}`);
86
+ logger.info(`Scope: ${scopeLabel}`);
87
+ logger.dim('');
88
+ }
89
+
90
+ /**
91
+ * Displays update results after completion.
92
+ *
93
+ * Shows success/failure status, backup location, migration status, and next steps.
94
+ *
95
+ * @param {Object} result - Update result from performUpdate()
96
+ * @param {string} scopeLabel - Label for scope (Global/Local)
97
+ * @param {boolean} isDryRun - Whether this was a dry run
98
+ * @private
99
+ */
100
+ function displayUpdateResults(result, scopeLabel, isDryRun = false) {
101
+ if (isDryRun) {
102
+ logger.heading(`Dry Run Results for ${scopeLabel} Installation`);
103
+ logger.dim('=====================================');
104
+ logger.dim('');
105
+ logger.info('No changes were made (dry run mode)');
106
+ } else {
107
+ logger.heading(`Update Results for ${scopeLabel} Installation`);
108
+ logger.dim('=====================================');
109
+ logger.dim('');
110
+ }
111
+
112
+ if (result.success) {
113
+ if (isDryRun) {
114
+ logger.success(`Would update to version ${result.version}`);
115
+ } else {
116
+ logger.success(`Updated to version ${result.version}`);
117
+ }
118
+ } else {
119
+ logger.error('Update failed');
120
+ }
121
+
122
+ // Display migration status if applicable
123
+ if (result.stats.structureMigrated) {
124
+ logger.dim('');
125
+ logger.success('Structure migrated: Old (command/gsd/) → New (commands/gsd/)');
126
+ if (result.stats.migrationBackup) {
127
+ logger.dim(` Migration backup: ${result.stats.migrationBackup}`);
128
+ }
129
+ } else if (result.stats.migrationSkipped) {
130
+ logger.dim('');
131
+ logger.warning('Structure migration was skipped (--skip-migration flag)');
132
+ }
133
+
134
+ if (result.errors && result.errors.length > 0) {
135
+ logger.dim('');
136
+ logger.info('Errors:');
137
+ for (const error of result.errors) {
138
+ logger.dim(` ✗ ${error}`);
139
+ }
140
+ }
141
+
142
+ logger.dim('');
143
+ if (isDryRun) {
144
+ logger.dim('To perform the actual update, run without --dry-run');
145
+ } else {
146
+ logger.dim('Next steps:');
147
+ logger.dim(" Run 'gsd-opencode list' to verify the installation");
148
+ logger.dim(" Run 'gsd-opencode check' to verify installation health");
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Main update command function.
154
+ *
155
+ * Orchestrates the update process:
156
+ * 1. Parse options and set verbose mode
157
+ * 2. Determine scopes to check (global, local, or both)
158
+ * 3. For each scope:
159
+ * - Check if installed
160
+ * - Create service instances
161
+ * - Check for available updates
162
+ * - Display version comparison
163
+ * - Prompt for confirmation (unless --force)
164
+ * - Perform update with progress indication
165
+ * - Display results
166
+ * 4. Return appropriate exit code
167
+ *
168
+ * @param {Object} options - Command options
169
+ * @param {boolean} [options.global] - Update global installation only
170
+ * @param {boolean} [options.local] - Update local installation only
171
+ * @param {boolean} [options.beta] - Update to beta version from @rokicool/gsd-opencode
172
+ * @param {boolean} [options.verbose] - Enable verbose output for debugging
173
+ * @param {string} [options.version] - Specific version to install
174
+ * @param {boolean} [options.force] - Skip confirmation prompt
175
+ * @param {boolean} [options.dryRun] - Show what would be done without making changes
176
+ * @param {boolean} [options.skipMigration] - Skip structure migration (not recommended)
177
+ * @returns {Promise<number>} Exit code (0=success, 1=error, 2=permission, 130=interrupted)
178
+ * @async
179
+ *
180
+ * @example
181
+ * // Update to latest stable
182
+ * const exitCode = await updateCommand({});
183
+ *
184
+ * // Update to latest beta
185
+ * const exitCode = await updateCommand({ beta: true });
186
+ *
187
+ * // Update to specific version
188
+ * const exitCode = await updateCommand({ version: '2.0.0' });
189
+ *
190
+ * // Update global only with force
191
+ * const exitCode = await updateCommand({ global: true, force: true });
192
+ */
193
+ export async function updateCommand(options = {}) {
194
+ const verbose = options.verbose || false;
195
+ setVerbose(verbose);
196
+
197
+ logger.debug('Starting update command');
198
+ logger.debug(`Options: global=${options.global}, local=${options.local}, beta=${options.beta}, version=${options.version}, force=${options.force}, verbose=${verbose}, dryRun=${options.dryRun}, skipMigration=${options.skipMigration}`);
199
+
200
+ try {
201
+ logger.heading('GSD-OpenCode Update');
202
+ logger.dim('===================');
203
+ logger.dim('');
204
+
205
+ // Determine scopes to check
206
+ const scopesToCheck = [];
207
+ if (options.global) {
208
+ scopesToCheck.push('global');
209
+ } else if (options.local) {
210
+ scopesToCheck.push('local');
211
+ } else {
212
+ scopesToCheck.push('global', 'local');
213
+ }
214
+
215
+ let anyInstalled = false;
216
+ let anyUpdated = false;
217
+ let anyFailed = false;
218
+
219
+ for (const scope of scopesToCheck) {
220
+ try {
221
+ const scopeManager = new ScopeManager({ scope });
222
+ const scopeLabel = scope.charAt(0).toUpperCase() + scope.slice(1);
223
+
224
+ logger.debug(`Checking ${scope} installation...`);
225
+
226
+ // Check if installed (unless --force which allows fresh install)
227
+ const isInstalled = await scopeManager.isInstalled();
228
+ if (!isInstalled && !options.force) {
229
+ logger.info(`No installation found at ${scopeLabel.toLowerCase()} scope`);
230
+ logger.dim('');
231
+ continue;
232
+ }
233
+
234
+ anyInstalled = true;
235
+
236
+ // Create service instances
237
+ const backupManager = new BackupManager(scopeManager, logger);
238
+ const fileOps = new FileOperations(scopeManager, logger);
239
+ const npmRegistry = new NpmRegistry({ logger });
240
+
241
+ // Determine package name
242
+ const packageName = options.beta ? '@rokicool/gsd-opencode' : 'gsd-opencode';
243
+
244
+ // Create UpdateService
245
+ const updateService = new UpdateService({
246
+ scopeManager,
247
+ backupManager,
248
+ fileOps,
249
+ npmRegistry,
250
+ logger,
251
+ packageName
252
+ });
253
+
254
+ // Check for updates
255
+ logger.debug('Checking for available updates...');
256
+ const checkResult = await updateService.checkForUpdate();
257
+
258
+ if (checkResult.error) {
259
+ logger.error(`Failed to check for updates: ${checkResult.error}`);
260
+ anyFailed = true;
261
+ continue;
262
+ }
263
+
264
+ // Handle already up to date
265
+ if (!checkResult.updateAvailable && !options.version) {
266
+ logger.success(`${scopeLabel} installation is up to date (${checkResult.currentVersion})`);
267
+ logger.dim('');
268
+ continue;
269
+ }
270
+
271
+ // Determine target version
272
+ const targetVersion = options.version || checkResult.latestVersion;
273
+
274
+ if (!targetVersion) {
275
+ logger.error('Could not determine target version');
276
+ anyFailed = true;
277
+ continue;
278
+ }
279
+
280
+ // Display update info
281
+ displayUpdateInfo(checkResult.currentVersion, targetVersion, scopeLabel);
282
+
283
+ // If specific version requested, validate it exists
284
+ if (options.version) {
285
+ const versionExists = await npmRegistry.versionExists(packageName, options.version);
286
+ if (!versionExists) {
287
+ logger.error(`Version ${options.version} does not exist for ${packageName}`);
288
+ anyFailed = true;
289
+ continue;
290
+ }
291
+ }
292
+
293
+ // Prompt for confirmation (unless --force)
294
+ if (!options.force) {
295
+ logger.debug('Requesting user confirmation...');
296
+ const confirmed = await promptConfirmation(
297
+ `Proceed with update to ${targetVersion}?`,
298
+ true
299
+ );
300
+
301
+ // Handle SIGINT (Ctrl+C) - user cancelled with interrupt
302
+ if (confirmed === null) {
303
+ logger.info('Update cancelled');
304
+ return ERROR_CODES.INTERRUPTED;
305
+ }
306
+
307
+ // Handle explicit "no" response
308
+ if (!confirmed) {
309
+ logger.info('Update cancelled');
310
+ return ERROR_CODES.SUCCESS;
311
+ }
312
+
313
+ logger.debug('User confirmed update');
314
+ } else {
315
+ logger.debug('Skipping confirmation (--force flag)');
316
+ }
317
+
318
+ // Perform update with progress indication
319
+ const spinner = ora({
320
+ text: 'Starting update...',
321
+ spinner: 'dots',
322
+ color: 'cyan'
323
+ }).start();
324
+
325
+ const updateResult = await updateService.performUpdate(targetVersion, {
326
+ onProgress: ({ phase, current, total, message, overallProgress }) => {
327
+ spinner.text = `${phase}: ${message} (${overallProgress}%)`;
328
+ },
329
+ dryRun: options.dryRun,
330
+ skipMigration: options.skipMigration
331
+ });
332
+
333
+ if (updateResult.success) {
334
+ spinner.succeed(`Updated to ${targetVersion}`);
335
+ } else {
336
+ spinner.fail('Update failed');
337
+ }
338
+
339
+ logger.dim('');
340
+
341
+ // Display results
342
+ displayUpdateResults(updateResult, scopeLabel, options.dryRun);
343
+
344
+ // Show backup location if created
345
+ if (updateResult.stats.backupCreated && !options.dryRun) {
346
+ const backupDir = backupManager.getBackupDir();
347
+ logger.dim(`Backup saved to: ${backupDir}`);
348
+ logger.dim('');
349
+ }
350
+
351
+ // Show warning if skipMigration was used
352
+ if (options.skipMigration) {
353
+ logger.warning('Structure migration was skipped. Old structure may cause issues.');
354
+ logger.dim(' Run "gsd-opencode update" without --skip-migration to migrate.');
355
+ logger.dim('');
356
+ }
357
+
358
+ // Track overall status
359
+ if (updateResult.success) {
360
+ anyUpdated = true;
361
+ } else {
362
+ anyFailed = true;
363
+ }
364
+
365
+ } catch (error) {
366
+ logger.error(`Failed to update ${scope} installation: ${error.message}`);
367
+ anyFailed = true;
368
+
369
+ if (verbose) {
370
+ logger.debug(error.stack);
371
+ }
372
+ }
373
+ }
374
+
375
+ // Overall status message
376
+ logger.dim('');
377
+
378
+ if (!anyInstalled && !options.force) {
379
+ logger.info('No GSD-OpenCode installation found to update');
380
+ logger.dim('');
381
+ logger.info("Run 'gsd-opencode install' to install");
382
+ logger.dim('');
383
+ return ERROR_CODES.SUCCESS;
384
+ }
385
+
386
+ if (anyFailed) {
387
+ logger.error('Some updates failed. Run gsd-opencode check for details.');
388
+ return ERROR_CODES.GENERAL_ERROR;
389
+ }
390
+
391
+ if (anyUpdated) {
392
+ logger.success('All updates completed successfully');
393
+ }
394
+
395
+ return ERROR_CODES.SUCCESS;
396
+
397
+ } catch (error) {
398
+ // Handle Ctrl+C during async operations (AbortPromptError from @inquirer/prompts)
399
+ if (error.name === 'AbortPromptError' || error.message?.includes('cancel')) {
400
+ logger.info('Update cancelled by user');
401
+ return ERROR_CODES.INTERRUPTED;
402
+ }
403
+
404
+ // Handle permission errors (EACCES)
405
+ if (error.code === 'EACCES') {
406
+ logger.error('Permission denied: Cannot access installation directory');
407
+ logger.dim('');
408
+ logger.dim('Suggestion: Check directory permissions or run with appropriate privileges');
409
+ return ERROR_CODES.PERMISSION_ERROR;
410
+ }
411
+
412
+ // Handle network errors
413
+ if (error.message?.includes('ENOTFOUND') || error.message?.includes('ECONNREFUSED')) {
414
+ logger.error('Network error: Unable to reach npm registry');
415
+ logger.dim('');
416
+ logger.dim('Suggestion: Check your internet connection and try again');
417
+ return ERROR_CODES.GENERAL_ERROR;
418
+ }
419
+
420
+ // Handle version not found
421
+ if (error.message?.includes('not found') || error.message?.includes('E404')) {
422
+ logger.error(`Package or version not found: ${error.message}`);
423
+ return ERROR_CODES.GENERAL_ERROR;
424
+ }
425
+
426
+ // Handle all other errors
427
+ logger.error(`Update failed: ${error.message}`);
428
+
429
+ if (verbose && error.stack) {
430
+ logger.dim(error.stack);
431
+ }
432
+
433
+ return ERROR_CODES.GENERAL_ERROR;
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Default export for the update command.
439
+ *
440
+ * @example
441
+ * import updateCommand from './commands/update.js';
442
+ * const exitCode = await updateCommand({ beta: true });
443
+ */
444
+ export default updateCommand;