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,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;
|