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,71 @@
1
+ /**
2
+ * Hash utility for file integrity checking.
3
+ *
4
+ * This module provides SHA-256 hashing functions for verifying file integrity
5
+ * during installation health checks. Uses Node.js built-in crypto module.
6
+ *
7
+ * @module hash
8
+ */
9
+
10
+ import crypto from 'crypto';
11
+ import fs from 'fs/promises';
12
+
13
+ /**
14
+ * Generates SHA-256 hash of a file's contents.
15
+ *
16
+ * Reads the file at the specified path and returns its SHA-256 hash.
17
+ * Returns null if the file doesn't exist or can't be read.
18
+ *
19
+ * @param {string} filePath - Absolute or relative path to the file
20
+ * @returns {Promise<string|null>} Hex-encoded SHA-256 hash, or null if file can't be read
21
+ *
22
+ * @example
23
+ * const hash = await hashFile('/path/to/file.txt');
24
+ * if (hash) {
25
+ * console.log(`File hash: ${hash}`);
26
+ * } else {
27
+ * console.log('File not found or unreadable');
28
+ * }
29
+ */
30
+ export async function hashFile(filePath) {
31
+ try {
32
+ const content = await fs.readFile(filePath);
33
+ return crypto.createHash('sha256').update(content).digest('hex');
34
+ } catch (error) {
35
+ // Return null for any error (file not found, permission denied, etc.)
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Generates SHA-256 hash of a string.
42
+ *
43
+ * Useful for comparing expected content hashes or generating
44
+ * checksums for verification purposes.
45
+ *
46
+ * @param {string} str - String to hash
47
+ * @returns {string} Hex-encoded SHA-256 hash
48
+ *
49
+ * @example
50
+ * const hash = hashString('hello world');
51
+ * console.log(hash); // 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
52
+ */
53
+ export function hashString(str) {
54
+ if (typeof str !== 'string') {
55
+ throw new TypeError('Input must be a string');
56
+ }
57
+ return crypto.createHash('sha256').update(str, 'utf-8').digest('hex');
58
+ }
59
+
60
+ /**
61
+ * Default export for the hash module.
62
+ *
63
+ * @example
64
+ * import { hashFile, hashString } from './utils/hash.js';
65
+ * const fileHash = await hashFile('/path/to/file.txt');
66
+ * const strHash = hashString('test content');
67
+ */
68
+ export default {
69
+ hashFile,
70
+ hashString
71
+ };
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Interactive prompt utilities for GSD-OpenCode CLI
3
+ *
4
+ * Provides user-friendly prompts for installation scope selection,
5
+ * confirmations, and repair decisions. All prompts handle Ctrl+C
6
+ * gracefully to ensure clean aborts with no side effects.
7
+ *
8
+ * @module interactive
9
+ */
10
+
11
+ import { select, confirm, input } from '@inquirer/prompts';
12
+
13
+ /**
14
+ * Prompts user to select installation scope
15
+ *
16
+ * Displays a select prompt with "Global" as the default option.
17
+ * Global installs to ~/.config/opencode/, local installs to ./.opencode/
18
+ *
19
+ * @returns {Promise<string|null>} 'global', 'local', or null if cancelled
20
+ * @example
21
+ * const scope = await promptInstallScope();
22
+ * if (scope === null) {
23
+ * console.log('Installation cancelled');
24
+ * process.exit(0);
25
+ * }
26
+ */
27
+ export async function promptInstallScope() {
28
+ try {
29
+ const answer = await select({
30
+ message: 'Where would you like to install GSD-OpenCode?',
31
+ choices: [
32
+ {
33
+ name: 'Global (~/.config/opencode/)',
34
+ value: 'global',
35
+ description: 'Install globally for all projects'
36
+ },
37
+ {
38
+ name: 'Local (./.opencode/)',
39
+ value: 'local',
40
+ description: 'Install locally in current directory'
41
+ }
42
+ ],
43
+ default: 'global'
44
+ });
45
+
46
+ return answer;
47
+ } catch (error) {
48
+ // Handle Ctrl+C (SIGINT) - user cancelled
49
+ if (error.name === 'AbortPromptError' || error.message?.includes('cancel')) {
50
+ return null;
51
+ }
52
+ // Re-throw unexpected errors
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Prompts user for a yes/no confirmation
59
+ *
60
+ * @param {string} message - The confirmation message to display
61
+ * @param {boolean} [defaultValue=true] - Default value if user just presses Enter
62
+ * @returns {Promise<boolean|null>} true/false, or null if cancelled
63
+ * @example
64
+ * const confirmed = await promptConfirmation('Remove existing installation?', true);
65
+ * if (confirmed === null) {
66
+ * console.log('Operation cancelled');
67
+ * process.exit(0);
68
+ * }
69
+ * if (confirmed) {
70
+ * // Proceed with removal
71
+ * }
72
+ */
73
+ export async function promptConfirmation(message, defaultValue = true) {
74
+ try {
75
+ const answer = await confirm({
76
+ message,
77
+ default: defaultValue
78
+ });
79
+
80
+ return answer;
81
+ } catch (error) {
82
+ // Handle Ctrl+C (SIGINT) - user cancelled
83
+ if (error.name === 'AbortPromptError' || error.message?.includes('cancel')) {
84
+ return null;
85
+ }
86
+ // Re-throw unexpected errors
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Prompts user to choose between repairing existing installation,
93
+ * performing a fresh install, or cancelling
94
+ *
95
+ * Called when a partial or broken installation is detected.
96
+ *
97
+ * @returns {Promise<string|null>} 'repair', 'fresh', 'cancel', or null if cancelled
98
+ * @example
99
+ * const choice = await promptRepairOrFresh();
100
+ * if (choice === null || choice === 'cancel') {
101
+ * console.log('Installation cancelled');
102
+ * process.exit(0);
103
+ * }
104
+ * if (choice === 'repair') {
105
+ * // Repair existing installation
106
+ * } else if (choice === 'fresh') {
107
+ * // Remove existing and perform fresh install
108
+ * }
109
+ */
110
+ export async function promptRepairOrFresh() {
111
+ try {
112
+ const answer = await select({
113
+ message: 'Existing installation detected. What would you like to do?',
114
+ choices: [
115
+ {
116
+ name: 'Repair existing installation',
117
+ value: 'repair',
118
+ description: 'Fix the existing installation without data loss'
119
+ },
120
+ {
121
+ name: 'Fresh install (remove existing)',
122
+ value: 'fresh',
123
+ description: 'Remove existing and start fresh'
124
+ },
125
+ {
126
+ name: 'Cancel',
127
+ value: 'cancel',
128
+ description: 'Abort installation'
129
+ }
130
+ ],
131
+ default: 'repair'
132
+ });
133
+
134
+ return answer;
135
+ } catch (error) {
136
+ // Handle Ctrl+C (SIGINT) - user cancelled
137
+ if (error.name === 'AbortPromptError' || error.message?.includes('cancel')) {
138
+ return 'cancel';
139
+ }
140
+ // Re-throw unexpected errors
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Prompts the user to type a confirmation word to proceed.
147
+ *
148
+ * The user must type the exact confirmation word to proceed. No retries - if the
149
+ * user enters anything else, the function immediately returns false.
150
+ *
151
+ * @param {string} message - The message to display before the prompt
152
+ * @param {string} confirmWord - The word user must type to confirm
153
+ * @returns {Promise<boolean|null>} true if confirmed, false if rejected, null if cancelled
154
+ * @example
155
+ * const confirmed = await promptTypedConfirmation(
156
+ * 'This will uninstall GSD-OpenCode. Type "yes" to confirm',
157
+ * 'yes'
158
+ * );
159
+ * if (confirmed === null) {
160
+ * console.log('Operation cancelled');
161
+ * process.exit(130);
162
+ * }
163
+ * if (!confirmed) {
164
+ * console.log('Confirmation failed');
165
+ * process.exit(0);
166
+ * }
167
+ */
168
+ export async function promptTypedConfirmation(message, confirmWord) {
169
+ const normalizedConfirmWord = confirmWord.toLowerCase();
170
+
171
+ try {
172
+ const answer = await input({
173
+ message: `${message} (type "${confirmWord}")`,
174
+ validate: (value) => {
175
+ if (!value || value.trim() === '') {
176
+ return 'Please enter the confirmation word';
177
+ }
178
+ return true;
179
+ }
180
+ });
181
+
182
+ const normalizedAnswer = answer.trim().toLowerCase();
183
+
184
+ if (normalizedAnswer === normalizedConfirmWord) {
185
+ return true;
186
+ }
187
+
188
+ // Wrong word entered - immediately return false
189
+ return false;
190
+ } catch (error) {
191
+ // Handle Ctrl+C (SIGINT) - user cancelled
192
+ if (error.name === 'AbortPromptError' || error.message?.includes('cancel')) {
193
+ return null;
194
+ }
195
+ // Re-throw unexpected errors
196
+ throw error;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Checks if the current environment supports interactive prompts
202
+ *
203
+ * Returns false if stdin is not a TTY (e.g., piped input, CI environment)
204
+ *
205
+ * @returns {boolean} true if interactive prompts are supported
206
+ */
207
+ export function isInteractive() {
208
+ return process.stdin.isTTY && process.stdout.isTTY;
209
+ }
210
+
211
+ /**
212
+ * Helper to handle prompt cancellation consistently
213
+ *
214
+ * Logs cancellation message and exits cleanly if prompts are cancelled.
215
+ *
216
+ * @param {string} [message='Operation cancelled'] - Message to display
217
+ * @param {number} [exitCode=0] - Exit code (0 for clean abort)
218
+ */
219
+ export function handleCancellation(message = 'Operation cancelled', exitCode = 0) {
220
+ console.log(message);
221
+ process.exit(exitCode);
222
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Logger utility for consistent terminal output styling.
3
+ *
4
+ * Uses chalk for color support and automatically respects NO_COLOR environment variable.
5
+ * All output goes to stderr to avoid polluting piped output.
6
+ *
7
+ * @module logger
8
+ */
9
+
10
+ import chalk from 'chalk';
11
+
12
+ /**
13
+ * Module-level verbose flag.
14
+ * When true, debug messages and full stack traces are shown.
15
+ * @type {boolean}
16
+ */
17
+ let verboseMode = false;
18
+
19
+ /**
20
+ * Set verbose mode for debug output.
21
+ * @param {boolean} enabled - Whether to enable verbose output
22
+ */
23
+ export function setVerbose(enabled) {
24
+ verboseMode = Boolean(enabled);
25
+ }
26
+
27
+ /**
28
+ * Get current verbose mode status.
29
+ * @returns {boolean} Current verbose mode
30
+ */
31
+ export function isVerbose() {
32
+ return verboseMode;
33
+ }
34
+
35
+ /**
36
+ * Logger object with styled output methods.
37
+ * All methods output to stderr to avoid breaking piped output.
38
+ */
39
+ export const logger = {
40
+ /**
41
+ * Print blue info message with ℹ symbol.
42
+ * @param {string} message - Message to display
43
+ */
44
+ info(message) {
45
+ console.error(chalk.blue('ℹ'), message);
46
+ },
47
+
48
+ /**
49
+ * Print green success message with ✓ symbol.
50
+ * @param {string} message - Message to display
51
+ */
52
+ success(message) {
53
+ console.error(chalk.green('✓'), message);
54
+ },
55
+
56
+ /**
57
+ * Print yellow warning message with ⚠ symbol.
58
+ * @param {string} message - Message to display
59
+ */
60
+ warning(message) {
61
+ console.error(chalk.yellow('⚠'), message);
62
+ },
63
+
64
+ /**
65
+ * Print red error message with ✗ symbol.
66
+ * In verbose mode, includes full stack trace if error object provided.
67
+ * @param {string} message - Error message to display
68
+ * @param {Error} [error] - Optional error object for stack trace
69
+ */
70
+ error(message, error) {
71
+ console.error(chalk.red('✗'), message);
72
+
73
+ if (verboseMode && error?.stack) {
74
+ console.error(chalk.dim(error.stack));
75
+ }
76
+ },
77
+
78
+ /**
79
+ * Print gray debug message (only shown in verbose mode).
80
+ * @param {string} message - Debug message to display
81
+ */
82
+ debug(message) {
83
+ if (verboseMode) {
84
+ console.error(chalk.gray('[debug]'), message);
85
+ }
86
+ },
87
+
88
+ /**
89
+ * Print bold white heading for sections.
90
+ * @param {string} text - Heading text
91
+ */
92
+ heading(text) {
93
+ console.error(chalk.bold.white(text));
94
+ },
95
+
96
+ /**
97
+ * Print dimmed text for secondary information.
98
+ * @param {string} text - Text to dim
99
+ */
100
+ dim(text) {
101
+ console.error(chalk.dim(text));
102
+ },
103
+
104
+ /**
105
+ * Print cyan inline code formatting.
106
+ * @param {string} text - Code text to format
107
+ */
108
+ code(text) {
109
+ console.error(chalk.cyan(text));
110
+ }
111
+ };
112
+
113
+ /**
114
+ * Colorize utility for inline color formatting.
115
+ * Returns chalk instance for flexible color usage.
116
+ * @type {object}
117
+ */
118
+ export const colorize = chalk;
119
+
120
+ /**
121
+ * Default export combining logger and utilities.
122
+ */
123
+ export default {
124
+ logger,
125
+ colorize,
126
+ setVerbose,
127
+ isVerbose
128
+ };
@@ -0,0 +1,255 @@
1
+ /**
2
+ * NPM Registry query utility for fetching package version information.
3
+ *
4
+ * Provides a clean abstraction for querying npm registry versions, supporting
5
+ * both public packages (gsd-opencode) and scoped packages (@rokicool/gsd-opencode).
6
+ * This utility is the foundation for the update command's version checking capabilities.
7
+ *
8
+ * @module npm-registry
9
+ */
10
+
11
+ import { exec } from 'child_process';
12
+ import { promisify } from 'util';
13
+
14
+ const execAsync = promisify(exec);
15
+
16
+ /**
17
+ * Valid npm package name pattern.
18
+ * Supports scoped packages like @scope/name and regular packages.
19
+ * @type {RegExp}
20
+ */
21
+ const VALID_PACKAGE_NAME = /^(?:@([^/]+)\/)?([^/]+)$/;
22
+
23
+ /**
24
+ * Utility class for querying npm registry version information.
25
+ *
26
+ * @example
27
+ * const npm = new NpmRegistry();
28
+ * const version = await npm.getLatestVersion('gsd-opencode');
29
+ * const allVersions = await npm.getAllVersions('@rokicool/gsd-opencode');
30
+ */
31
+ export class NpmRegistry {
32
+ /**
33
+ * Creates a new NpmRegistry instance.
34
+ * @param {Object} [options={}] - Configuration options
35
+ * @param {Object} [options.logger] - Logger instance for output (defaults to console)
36
+ */
37
+ constructor(options = {}) {
38
+ this.logger = options.logger || console;
39
+ }
40
+
41
+ /**
42
+ * Validates a package name to prevent command injection.
43
+ *
44
+ * @param {string} packageName - The package name to validate
45
+ * @returns {boolean} True if valid, false otherwise
46
+ * @private
47
+ */
48
+ _validatePackageName(packageName) {
49
+ if (!packageName || typeof packageName !== 'string') {
50
+ return false;
51
+ }
52
+ return VALID_PACKAGE_NAME.test(packageName);
53
+ }
54
+
55
+ /**
56
+ * Escapes a package name for safe use in shell commands.
57
+ *
58
+ * @param {string} packageName - The package name to escape
59
+ * @returns {string} Escaped package name
60
+ * @private
61
+ */
62
+ _escapePackageName(packageName) {
63
+ // Replace any potentially dangerous characters
64
+ return packageName.replace(/[^a-zA-Z0-9@._/-]/g, '');
65
+ }
66
+
67
+ /**
68
+ * Gets the latest version of a package from npm registry.
69
+ *
70
+ * @param {string} packageName - The package name (e.g., 'gsd-opencode' or '@rokicool/gsd-opencode')
71
+ * @returns {Promise<string|null>} The latest version string (e.g., '1.9.2') or null on error
72
+ * @example
73
+ * const npm = new NpmRegistry();
74
+ * const version = await npm.getLatestVersion('gsd-opencode');
75
+ * console.log(version); // '1.9.2'
76
+ */
77
+ async getLatestVersion(packageName) {
78
+ if (!this._validatePackageName(packageName)) {
79
+ this.logger.error(`Invalid package name: ${packageName}`);
80
+ return null;
81
+ }
82
+
83
+ const escapedName = this._escapePackageName(packageName);
84
+
85
+ try {
86
+ const { stdout } = await execAsync(`npm view ${escapedName} version`);
87
+ const version = stdout.trim();
88
+ return version;
89
+ } catch (error) {
90
+ this._handleError('getLatestVersion', packageName, error);
91
+ return null;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Gets all available versions of a package from npm registry.
97
+ *
98
+ * @param {string} packageName - The package name (e.g., 'gsd-opencode' or '@rokicool/gsd-opencode')
99
+ * @returns {Promise<string[]>} Array of version strings sorted newest first, empty array on error
100
+ * @example
101
+ * const npm = new NpmRegistry();
102
+ * const versions = await npm.getAllVersions('gsd-opencode');
103
+ * console.log(versions); // ['1.9.2', '1.9.1', '1.9.0', ...]
104
+ */
105
+ async getAllVersions(packageName) {
106
+ if (!this._validatePackageName(packageName)) {
107
+ this.logger.error(`Invalid package name: ${packageName}`);
108
+ return [];
109
+ }
110
+
111
+ const escapedName = this._escapePackageName(packageName);
112
+
113
+ try {
114
+ const { stdout } = await execAsync(`npm view ${escapedName} versions --json`);
115
+ const versions = JSON.parse(stdout);
116
+
117
+ if (!Array.isArray(versions)) {
118
+ this.logger.error(`Unexpected response format for ${packageName}`);
119
+ return [];
120
+ }
121
+
122
+ // Sort versions newest first using compareVersions
123
+ return versions.sort((a, b) => -this.compareVersions(a, b));
124
+ } catch (error) {
125
+ this._handleError('getAllVersions', packageName, error);
126
+ return [];
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Checks if a specific version of a package exists in npm registry.
132
+ *
133
+ * @param {string} packageName - The package name
134
+ * @param {string} version - The version to check (e.g., '1.9.2')
135
+ * @returns {Promise<boolean>} True if the version exists, false otherwise
136
+ * @example
137
+ * const npm = new NpmRegistry();
138
+ * const exists = await npm.versionExists('gsd-opencode', '1.9.2');
139
+ * console.log(exists); // true
140
+ */
141
+ async versionExists(packageName, version) {
142
+ if (!this._validatePackageName(packageName)) {
143
+ this.logger.error(`Invalid package name: ${packageName}`);
144
+ return false;
145
+ }
146
+
147
+ if (!version || typeof version !== 'string') {
148
+ this.logger.error(`Invalid version: ${version}`);
149
+ return false;
150
+ }
151
+
152
+ const escapedName = this._escapePackageName(packageName);
153
+ const escapedVersion = version.replace(/[^0-9.a-zA-Z-]/g, '');
154
+
155
+ try {
156
+ // Try to view the specific version - if it exists, npm returns the version
157
+ await execAsync(`npm view ${escapedName}@${escapedVersion} version`);
158
+ return true;
159
+ } catch (error) {
160
+ // Version doesn't exist or other error
161
+ return false;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Compares two semantic version strings.
167
+ *
168
+ * Supports standard semver (e.g., '1.9.2') and pre-release versions
169
+ * (e.g., '1.9.2-dev-8a05'). Pre-release versions are considered
170
+ * lower than their release counterparts.
171
+ *
172
+ * @param {string} v1 - First version string
173
+ * @param {string} v2 - Second version string
174
+ * @returns {number} -1 if v1 < v2, 0 if equal, 1 if v1 > v2
175
+ * @example
176
+ * const npm = new NpmRegistry();
177
+ * npm.compareVersions('1.9.2', '1.9.1'); // 1
178
+ * npm.compareVersions('1.9.2-dev-8a05', '1.9.2'); // -1
179
+ * npm.compareVersions('1.9.2', '1.9.2'); // 0
180
+ */
181
+ compareVersions(v1, v2) {
182
+ if (!v1 || !v2) {
183
+ return 0;
184
+ }
185
+
186
+ // Parse version strings into components
187
+ const parseVersion = (v) => {
188
+ // Remove 'v' prefix if present
189
+ const clean = v.replace(/^v/, '');
190
+
191
+ // Split into main version and pre-release parts
192
+ const [main, prerelease] = clean.split('-');
193
+ const parts = main.split('.').map(Number);
194
+
195
+ return {
196
+ parts: parts.length >= 3 ? parts : [...parts, 0, 0, 0].slice(0, 3),
197
+ prerelease: prerelease || null,
198
+ hasPrerelease: !!prerelease
199
+ };
200
+ };
201
+
202
+ const v1Parsed = parseVersion(v1);
203
+ const v2Parsed = parseVersion(v2);
204
+
205
+ // Compare main version parts
206
+ for (let i = 0; i < 3; i++) {
207
+ const p1 = v1Parsed.parts[i] || 0;
208
+ const p2 = v2Parsed.parts[i] || 0;
209
+
210
+ if (p1 > p2) return 1;
211
+ if (p1 < p2) return -1;
212
+ }
213
+
214
+ // Main versions are equal, check pre-release status
215
+ // A version without pre-release is greater than one with pre-release
216
+ if (!v1Parsed.hasPrerelease && v2Parsed.hasPrerelease) return 1;
217
+ if (v1Parsed.hasPrerelease && !v2Parsed.hasPrerelease) return -1;
218
+
219
+ // Both have pre-release, compare lexicographically
220
+ if (v1Parsed.hasPrerelease && v2Parsed.hasPrerelease) {
221
+ if (v1Parsed.prerelease > v2Parsed.prerelease) return 1;
222
+ if (v1Parsed.prerelease < v2Parsed.prerelease) return -1;
223
+ }
224
+
225
+ return 0;
226
+ }
227
+
228
+ /**
229
+ * Handles errors from npm commands with appropriate logging.
230
+ *
231
+ * @param {string} operation - The operation that failed
232
+ * @param {string} packageName - The package being queried
233
+ * @param {Error} error - The error object
234
+ * @private
235
+ */
236
+ _handleError(operation, packageName, error) {
237
+ const errorMessage = error.message || '';
238
+
239
+ if (errorMessage.includes('E404') || errorMessage.includes('not found')) {
240
+ this.logger.error(`Package not found: ${packageName}`);
241
+ } else if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('ECONNREFUSED')) {
242
+ this.logger.error(`Network error: Unable to reach npm registry`);
243
+ } else if (errorMessage.includes('npm ERR!') && errorMessage.includes('not in the npm registry')) {
244
+ this.logger.error(`Package not in npm registry: ${packageName}`);
245
+ } else {
246
+ this.logger.error(`Failed to ${operation} for ${packageName}: ${errorMessage}`);
247
+ }
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Default export of NpmRegistry class.
253
+ * @type {typeof NpmRegistry}
254
+ */
255
+ export default NpmRegistry;