@loxia-labs/loxia-autopilot-one 1.0.1
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/LICENSE +267 -0
- package/README.md +509 -0
- package/bin/cli.js +117 -0
- package/package.json +94 -0
- package/scripts/install-scanners.js +236 -0
- package/src/analyzers/CSSAnalyzer.js +297 -0
- package/src/analyzers/ConfigValidator.js +690 -0
- package/src/analyzers/ESLintAnalyzer.js +320 -0
- package/src/analyzers/JavaScriptAnalyzer.js +261 -0
- package/src/analyzers/PrettierFormatter.js +247 -0
- package/src/analyzers/PythonAnalyzer.js +266 -0
- package/src/analyzers/SecurityAnalyzer.js +729 -0
- package/src/analyzers/TypeScriptAnalyzer.js +247 -0
- package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
- package/src/analyzers/codeCloneDetector/detector.js +203 -0
- package/src/analyzers/codeCloneDetector/index.js +160 -0
- package/src/analyzers/codeCloneDetector/parser.js +199 -0
- package/src/analyzers/codeCloneDetector/reporter.js +148 -0
- package/src/analyzers/codeCloneDetector/scanner.js +59 -0
- package/src/core/agentPool.js +1474 -0
- package/src/core/agentScheduler.js +2147 -0
- package/src/core/contextManager.js +709 -0
- package/src/core/messageProcessor.js +732 -0
- package/src/core/orchestrator.js +548 -0
- package/src/core/stateManager.js +877 -0
- package/src/index.js +631 -0
- package/src/interfaces/cli.js +549 -0
- package/src/interfaces/webServer.js +2162 -0
- package/src/modules/fileExplorer/controller.js +280 -0
- package/src/modules/fileExplorer/index.js +37 -0
- package/src/modules/fileExplorer/middleware.js +92 -0
- package/src/modules/fileExplorer/routes.js +125 -0
- package/src/modules/fileExplorer/types.js +44 -0
- package/src/services/aiService.js +1232 -0
- package/src/services/apiKeyManager.js +164 -0
- package/src/services/benchmarkService.js +366 -0
- package/src/services/budgetService.js +539 -0
- package/src/services/contextInjectionService.js +247 -0
- package/src/services/conversationCompactionService.js +637 -0
- package/src/services/errorHandler.js +810 -0
- package/src/services/fileAttachmentService.js +544 -0
- package/src/services/modelRouterService.js +366 -0
- package/src/services/modelsService.js +322 -0
- package/src/services/qualityInspector.js +796 -0
- package/src/services/tokenCountingService.js +536 -0
- package/src/tools/agentCommunicationTool.js +1344 -0
- package/src/tools/agentDelayTool.js +485 -0
- package/src/tools/asyncToolManager.js +604 -0
- package/src/tools/baseTool.js +800 -0
- package/src/tools/browserTool.js +920 -0
- package/src/tools/cloneDetectionTool.js +621 -0
- package/src/tools/dependencyResolverTool.js +1215 -0
- package/src/tools/fileContentReplaceTool.js +875 -0
- package/src/tools/fileSystemTool.js +1107 -0
- package/src/tools/fileTreeTool.js +853 -0
- package/src/tools/imageTool.js +901 -0
- package/src/tools/importAnalyzerTool.js +1060 -0
- package/src/tools/jobDoneTool.js +248 -0
- package/src/tools/seekTool.js +956 -0
- package/src/tools/staticAnalysisTool.js +1778 -0
- package/src/tools/taskManagerTool.js +2873 -0
- package/src/tools/terminalTool.js +2304 -0
- package/src/tools/webTool.js +1430 -0
- package/src/types/agent.js +519 -0
- package/src/types/contextReference.js +972 -0
- package/src/types/conversation.js +730 -0
- package/src/types/toolCommand.js +747 -0
- package/src/utilities/attachmentValidator.js +292 -0
- package/src/utilities/configManager.js +582 -0
- package/src/utilities/constants.js +722 -0
- package/src/utilities/directoryAccessManager.js +535 -0
- package/src/utilities/fileProcessor.js +307 -0
- package/src/utilities/logger.js +436 -0
- package/src/utilities/tagParser.js +1246 -0
- package/src/utilities/toolConstants.js +317 -0
- package/web-ui/build/index.html +15 -0
- package/web-ui/build/logo.png +0 -0
- package/web-ui/build/logo2.png +0 -0
- package/web-ui/build/static/index-CjkkcnFA.js +344 -0
- package/web-ui/build/static/index-Dy2bYbOa.css +1 -0
|
@@ -0,0 +1,1215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file tools/dependencyResolverTool.js
|
|
3
|
+
* @description Modern tool for resolving Node.js dependency conflicts by checking and updating to latest compatible versions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { promises as fs } from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { exec as execCb } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
import { BaseTool } from './baseTool.js';
|
|
11
|
+
import TagParser from '../utilities/tagParser.js';
|
|
12
|
+
|
|
13
|
+
const exec = promisify(execCb);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration constants for the dependency resolver
|
|
17
|
+
*/
|
|
18
|
+
const RESOLVER_CONFIG = {
|
|
19
|
+
DEFAULT_MODE: 'check',
|
|
20
|
+
VALID_MODES: ['check', 'fix', 'auto'],
|
|
21
|
+
NPM_COMMAND_TIMEOUT: 300000, // 5 minutes for npm commands
|
|
22
|
+
REGISTRY_TIMEOUT: 10000, // 10 seconds for registry requests
|
|
23
|
+
MAX_CONCURRENT_CHECKS: 5, // Max parallel registry checks
|
|
24
|
+
BACKUP_EXTENSION: '.backup.json', // Backup file extension
|
|
25
|
+
CREATE_BACKUPS: true, // Always create backups
|
|
26
|
+
RETRY_ATTEMPTS: 3, // Registry request retry attempts
|
|
27
|
+
RETRY_DELAY: 1000, // Delay between retries (ms)
|
|
28
|
+
MAX_DEPENDENCIES: 500, // Safety limit
|
|
29
|
+
NPM_REGISTRY_URL: 'https://registry.npmjs.org'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* DependencyResolverTool - Modern implementation
|
|
34
|
+
* Resolves Node.js package dependency conflicts with improved security and reliability
|
|
35
|
+
*/
|
|
36
|
+
export class DependencyResolverTool extends BaseTool {
|
|
37
|
+
/**
|
|
38
|
+
* Get tool description for agent system prompt
|
|
39
|
+
* @returns {string} Formatted tool description
|
|
40
|
+
*/
|
|
41
|
+
getDescription() {
|
|
42
|
+
return `Tool: Dependency Resolver - Resolve Node.js package dependency conflicts
|
|
43
|
+
|
|
44
|
+
**Purpose:** Checks npm dependencies for updates and optionally updates package.json to latest compatible versions automatically.
|
|
45
|
+
|
|
46
|
+
**Invocation Syntax:**
|
|
47
|
+
|
|
48
|
+
XML Format:
|
|
49
|
+
\`\`\`xml
|
|
50
|
+
<dependency-resolve>
|
|
51
|
+
<path>./my-project</path>
|
|
52
|
+
<mode>check</mode>
|
|
53
|
+
<include-dev>true</include-dev>
|
|
54
|
+
</dependency-resolve>
|
|
55
|
+
\`\`\`
|
|
56
|
+
|
|
57
|
+
JSON Format:
|
|
58
|
+
\`\`\`json
|
|
59
|
+
{
|
|
60
|
+
"toolId": "dependency-resolver",
|
|
61
|
+
"parameters": {
|
|
62
|
+
"path": "./my-project",
|
|
63
|
+
"mode": "check",
|
|
64
|
+
"includeDev": true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
**Parameters:**
|
|
70
|
+
- **path** (string, optional): Path to project directory with package.json. Default: "."
|
|
71
|
+
- **mode** (string, optional): Operation mode. Options:
|
|
72
|
+
- "check" - Only check for updates (default)
|
|
73
|
+
- "fix" - Update package.json and run npm install
|
|
74
|
+
- "auto" - Automatically fix all conflicts
|
|
75
|
+
- **includeDev** (boolean, optional): Include devDependencies. Default: true
|
|
76
|
+
|
|
77
|
+
**What It Does:**
|
|
78
|
+
- Checks npm registry for latest compatible versions
|
|
79
|
+
- Respects semver ranges (^, ~, >=, etc.)
|
|
80
|
+
- Creates automatic backups before modifications
|
|
81
|
+
- Runs npm install after updates (in fix/auto mode)
|
|
82
|
+
- Provides detailed update report
|
|
83
|
+
|
|
84
|
+
**Examples:**
|
|
85
|
+
|
|
86
|
+
1. Check for updates:
|
|
87
|
+
\`\`\`xml
|
|
88
|
+
<dependency-resolver>
|
|
89
|
+
<mode>check</mode>
|
|
90
|
+
</dependency-resolver>
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
2. Fix outdated dependencies:
|
|
94
|
+
\`\`\`xml
|
|
95
|
+
<dependency-resolver>
|
|
96
|
+
<path>./my-project</path>
|
|
97
|
+
<mode>fix</mode>
|
|
98
|
+
</dependency-resolver>
|
|
99
|
+
\`\`\`
|
|
100
|
+
|
|
101
|
+
3. Auto-fix with devDependencies:
|
|
102
|
+
\`\`\`json
|
|
103
|
+
{
|
|
104
|
+
"toolId": "dependency-resolver",
|
|
105
|
+
"parameters": { "mode": "auto", "includeDev": true }
|
|
106
|
+
}
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
**Notes:**
|
|
110
|
+
- Always creates backup (.backup.json) before modifications
|
|
111
|
+
- Network requests have timeout and retry logic
|
|
112
|
+
- npm install runs with timeout protection (5 minutes max)
|
|
113
|
+
- Supports complex semver ranges (||, &&, etc.)`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse tool parameters from raw content (XML or JSON)
|
|
118
|
+
* @param {string|Object} content - Raw tool content or parsed object
|
|
119
|
+
* @returns {Object} Parsed parameters
|
|
120
|
+
*/
|
|
121
|
+
parseParameters(content) {
|
|
122
|
+
// If already an object, validate and return
|
|
123
|
+
if (typeof content === 'object' && content !== null) {
|
|
124
|
+
return {
|
|
125
|
+
path: content.path || '.',
|
|
126
|
+
mode: content.mode || RESOLVER_CONFIG.DEFAULT_MODE,
|
|
127
|
+
includeDev: content.includeDev !== undefined ? content.includeDev : true
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Parse XML content
|
|
132
|
+
if (typeof content === 'string') {
|
|
133
|
+
// Try modern XML format first: <dependency-resolve>...</dependency-resolve>
|
|
134
|
+
const modernPattern = /<dependency-resolve([^>]*)>([\s\S]*?)<\/dependency-resolve>/i;
|
|
135
|
+
const modernMatch = modernPattern.exec(content);
|
|
136
|
+
|
|
137
|
+
if (modernMatch) {
|
|
138
|
+
const attributesStr = modernMatch[1];
|
|
139
|
+
const innerContent = modernMatch[2];
|
|
140
|
+
|
|
141
|
+
// Parse attributes from opening tag
|
|
142
|
+
const pathAttr = /path=["']([^"']*)["']/i.exec(attributesStr);
|
|
143
|
+
const modeAttr = /mode=["']([^"']*)["']/i.exec(attributesStr);
|
|
144
|
+
const includeDevAttr = /include-dev=["']([^"']*)["']/i.exec(attributesStr);
|
|
145
|
+
|
|
146
|
+
// Extract from inner content
|
|
147
|
+
const pathPattern = /<path>(.*?)<\/path>/i;
|
|
148
|
+
const pathMatch = pathPattern.exec(innerContent);
|
|
149
|
+
|
|
150
|
+
const modePattern = /<mode>(.*?)<\/mode>/i;
|
|
151
|
+
const modeMatch = modePattern.exec(innerContent);
|
|
152
|
+
|
|
153
|
+
const includeDevPattern = /<include-dev>(.*?)<\/include-dev>/i;
|
|
154
|
+
const includeDevMatch = includeDevPattern.exec(innerContent);
|
|
155
|
+
|
|
156
|
+
// Content takes precedence over attributes
|
|
157
|
+
const extractedPath = (pathMatch ? pathMatch[1].trim() : null) || (pathAttr ? pathAttr[1] : '.');
|
|
158
|
+
const extractedMode = (modeMatch ? modeMatch[1].trim() : null) || (modeAttr ? modeAttr[1] : RESOLVER_CONFIG.DEFAULT_MODE);
|
|
159
|
+
const extractedIncludeDev = (includeDevMatch ? includeDevMatch[1].trim() : null) || (includeDevAttr ? includeDevAttr[1] : 'true');
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
path: extractedPath,
|
|
163
|
+
mode: extractedMode,
|
|
164
|
+
includeDev: this._parseBoolean(extractedIncludeDev, true)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Try legacy format: [resolve path="..." mode="..."]
|
|
169
|
+
const legacyPattern = /\[resolve\s+([^\]]*)\]/i;
|
|
170
|
+
const legacyMatch = legacyPattern.exec(content);
|
|
171
|
+
|
|
172
|
+
if (legacyMatch) {
|
|
173
|
+
const attrString = legacyMatch[1];
|
|
174
|
+
|
|
175
|
+
// Parse attributes manually
|
|
176
|
+
const pathAttr = /path=["']([^"']*)["']/i.exec(attrString);
|
|
177
|
+
const modeAttr = /mode=["']([^"']*)["']/i.exec(attrString);
|
|
178
|
+
const includeDevAttr = /include-dev=["']([^"']*)["']/i.exec(attrString);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
path: pathAttr ? pathAttr[1] : '.',
|
|
182
|
+
mode: modeAttr ? modeAttr[1] : RESOLVER_CONFIG.DEFAULT_MODE,
|
|
183
|
+
includeDev: this._parseBoolean(includeDevAttr ? includeDevAttr[1] : 'true', true)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
throw new Error('Invalid dependency-resolve format. Use <dependency-resolve> tags or JSON format.');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
throw new Error('Invalid parameter format. Expected string (XML) or object (JSON).');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Parse boolean from string or boolean
|
|
195
|
+
* @param {any} value - Value to parse
|
|
196
|
+
* @param {boolean} defaultValue - Default if undefined
|
|
197
|
+
* @returns {boolean}
|
|
198
|
+
* @private
|
|
199
|
+
*/
|
|
200
|
+
_parseBoolean(value, defaultValue = false) {
|
|
201
|
+
if (value === undefined || value === null) return defaultValue;
|
|
202
|
+
if (typeof value === 'boolean') return value;
|
|
203
|
+
if (typeof value === 'string') {
|
|
204
|
+
return value.toLowerCase() === 'true' || value === '1';
|
|
205
|
+
}
|
|
206
|
+
return defaultValue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Validate parameters
|
|
211
|
+
* @param {Object} params - Parameters to validate
|
|
212
|
+
* @throws {Error} If validation fails
|
|
213
|
+
* @private
|
|
214
|
+
*/
|
|
215
|
+
_validateParameters(params) {
|
|
216
|
+
if (!params || typeof params !== 'object') {
|
|
217
|
+
throw new Error('Parameters must be an object');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (params.path && typeof params.path !== 'string') {
|
|
221
|
+
throw new Error('path must be a string');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (params.mode && !RESOLVER_CONFIG.VALID_MODES.includes(params.mode)) {
|
|
225
|
+
throw new Error(`Invalid mode: ${params.mode}. Must be one of: ${RESOLVER_CONFIG.VALID_MODES.join(', ')}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (params.includeDev !== undefined && typeof params.includeDev !== 'boolean') {
|
|
229
|
+
throw new Error('includeDev must be a boolean');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Validate and resolve file path
|
|
235
|
+
* @param {string} targetPath - Target path from parameters
|
|
236
|
+
* @param {Object} context - Execution context
|
|
237
|
+
* @returns {string} Resolved absolute path
|
|
238
|
+
* @throws {Error} If path is invalid or inaccessible
|
|
239
|
+
* @private
|
|
240
|
+
*/
|
|
241
|
+
_resolveAndValidatePath(targetPath, context) {
|
|
242
|
+
const { projectDir, directoryAccess } = context;
|
|
243
|
+
|
|
244
|
+
// Determine working directory
|
|
245
|
+
let workingDirectory = projectDir || process.cwd();
|
|
246
|
+
|
|
247
|
+
if (directoryAccess && directoryAccess.workingDirectory) {
|
|
248
|
+
workingDirectory = directoryAccess.workingDirectory;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Resolve the target path
|
|
252
|
+
const resolvedPath = path.isAbsolute(targetPath)
|
|
253
|
+
? path.normalize(targetPath)
|
|
254
|
+
: path.normalize(path.join(workingDirectory, targetPath));
|
|
255
|
+
|
|
256
|
+
// Security: Check for path traversal
|
|
257
|
+
const realWorkingDir = path.normalize(workingDirectory);
|
|
258
|
+
if (!resolvedPath.startsWith(realWorkingDir)) {
|
|
259
|
+
throw new Error(`Path traversal detected: ${targetPath} resolves outside working directory`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return resolvedPath;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Create backup of package.json
|
|
267
|
+
* @param {string} pkgPath - Path to package.json
|
|
268
|
+
* @returns {Promise<string|null>} Backup file path or null if failed
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
async _createBackup(pkgPath) {
|
|
272
|
+
if (!RESOLVER_CONFIG.CREATE_BACKUPS) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const backupPath = pkgPath + RESOLVER_CONFIG.BACKUP_EXTENSION;
|
|
278
|
+
await fs.copyFile(pkgPath, backupPath);
|
|
279
|
+
this.logger?.info('Created backup', { backupPath });
|
|
280
|
+
return backupPath;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
this.logger?.warn('Failed to create backup', { error: error.message });
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Fetch package info from npm registry with retries
|
|
289
|
+
* @param {string} packageName - Package name
|
|
290
|
+
* @returns {Promise<Object|null>} Package data or null if failed
|
|
291
|
+
* @private
|
|
292
|
+
*/
|
|
293
|
+
async _fetchPackageInfo(packageName) {
|
|
294
|
+
const url = `${RESOLVER_CONFIG.NPM_REGISTRY_URL}/${encodeURIComponent(packageName)}`;
|
|
295
|
+
|
|
296
|
+
for (let attempt = 1; attempt <= RESOLVER_CONFIG.RETRY_ATTEMPTS; attempt++) {
|
|
297
|
+
try {
|
|
298
|
+
const controller = new AbortController();
|
|
299
|
+
const timeout = setTimeout(() => controller.abort(), RESOLVER_CONFIG.REGISTRY_TIMEOUT);
|
|
300
|
+
|
|
301
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
302
|
+
clearTimeout(timeout);
|
|
303
|
+
|
|
304
|
+
if (!response.ok) {
|
|
305
|
+
if (response.status === 404) {
|
|
306
|
+
this.logger?.warn(`Package not found: ${packageName}`);
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const data = await response.json();
|
|
313
|
+
|
|
314
|
+
// Validate response structure
|
|
315
|
+
if (!data || typeof data !== 'object' || !data['dist-tags']) {
|
|
316
|
+
throw new Error('Invalid registry response format');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return data;
|
|
320
|
+
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (attempt < RESOLVER_CONFIG.RETRY_ATTEMPTS) {
|
|
323
|
+
this.logger?.debug(`Retry ${attempt}/${RESOLVER_CONFIG.RETRY_ATTEMPTS} for ${packageName}`);
|
|
324
|
+
await new Promise(resolve => setTimeout(resolve, RESOLVER_CONFIG.RETRY_DELAY * attempt));
|
|
325
|
+
} else {
|
|
326
|
+
this.logger?.error(`Failed to fetch ${packageName}:`, error.message);
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get latest compatible version for a package
|
|
337
|
+
* Enhanced self-contained semver logic supporting:
|
|
338
|
+
* - Caret ranges (^) with 0.x.y and 0.0.x special cases
|
|
339
|
+
* - Tilde ranges (~)
|
|
340
|
+
* - Comparison operators (>, >=, <, <=)
|
|
341
|
+
* - X-ranges (4.x, 4.*, etc.)
|
|
342
|
+
* - Pre-release versions
|
|
343
|
+
* - Complex range expressions (AND/OR)
|
|
344
|
+
* - Exact versions
|
|
345
|
+
*
|
|
346
|
+
* @param {string} packageName - Package name
|
|
347
|
+
* @param {string} currentRange - Current version range
|
|
348
|
+
* @returns {Promise<string|null>} Latest version or null if no update needed
|
|
349
|
+
* @private
|
|
350
|
+
*/
|
|
351
|
+
async _getLatestCompatibleVersion(packageName, currentRange) {
|
|
352
|
+
const data = await this._fetchPackageInfo(packageName);
|
|
353
|
+
|
|
354
|
+
if (!data) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const latest = data['dist-tags']?.latest;
|
|
359
|
+
|
|
360
|
+
if (!latest) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Parse latest version
|
|
365
|
+
const latestParsed = this._parseVersion(latest);
|
|
366
|
+
|
|
367
|
+
if (!latestParsed) {
|
|
368
|
+
return null; // Can't parse latest
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Check if it's a complex range (AND/OR)
|
|
372
|
+
if (this._isComplexRange(currentRange)) {
|
|
373
|
+
const complexRange = this._parseComplexRange(currentRange);
|
|
374
|
+
const isUpdateAvailable = this._satisfiesComplexRange(complexRange, latestParsed);
|
|
375
|
+
|
|
376
|
+
if (isUpdateAvailable) {
|
|
377
|
+
// For complex ranges, preserve the original format
|
|
378
|
+
return latest;
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Simple range - parse normally
|
|
384
|
+
const rangeInfo = this._parseVersionRange(currentRange);
|
|
385
|
+
|
|
386
|
+
if (!rangeInfo) {
|
|
387
|
+
return null; // Can't parse, skip
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Check if update is available and compatible
|
|
391
|
+
const isUpdateAvailable = this._isUpdateAvailable(rangeInfo, latestParsed);
|
|
392
|
+
|
|
393
|
+
if (isUpdateAvailable) {
|
|
394
|
+
// Preserve the original prefix
|
|
395
|
+
return rangeInfo.prefix + latest;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Parse a version string into components including pre-release and build metadata
|
|
403
|
+
* @param {string} version - Version string (e.g., "4.17.1", "1.0.0-alpha.1", "1.0.0+build.123")
|
|
404
|
+
* @returns {Object|null} Parsed version or null
|
|
405
|
+
* @private
|
|
406
|
+
*/
|
|
407
|
+
_parseVersion(version) {
|
|
408
|
+
// Match: major.minor.patch[-prerelease][+build]
|
|
409
|
+
// Pre-release and build are optional
|
|
410
|
+
const pattern = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
411
|
+
const match = pattern.exec(version);
|
|
412
|
+
|
|
413
|
+
if (!match) {
|
|
414
|
+
// Fallback: try simple X.Y.Z pattern without pre-release/build
|
|
415
|
+
const simpleMatch = version.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
416
|
+
if (simpleMatch) {
|
|
417
|
+
return {
|
|
418
|
+
major: parseInt(simpleMatch[1], 10),
|
|
419
|
+
minor: parseInt(simpleMatch[2], 10),
|
|
420
|
+
patch: parseInt(simpleMatch[3], 10),
|
|
421
|
+
prerelease: null,
|
|
422
|
+
build: null
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
major: parseInt(match[1], 10),
|
|
430
|
+
minor: parseInt(match[2], 10),
|
|
431
|
+
patch: parseInt(match[3], 10),
|
|
432
|
+
prerelease: match[4] ? match[4].split('.') : null,
|
|
433
|
+
build: match[5] || null
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Compare two versions according to semver rules
|
|
439
|
+
* Returns: < 0 if v1 < v2, 0 if v1 === v2, > 0 if v1 > v2
|
|
440
|
+
* Handles pre-release versions correctly:
|
|
441
|
+
* - 1.0.0 > 1.0.0-alpha (release > pre-release)
|
|
442
|
+
* - 1.0.0-alpha < 1.0.0-beta (lexical comparison)
|
|
443
|
+
* - 1.0.0-1 < 1.0.0-2 (numeric comparison)
|
|
444
|
+
* @param {Object} v1 - First version
|
|
445
|
+
* @param {Object} v2 - Second version
|
|
446
|
+
* @returns {number} Comparison result
|
|
447
|
+
* @private
|
|
448
|
+
*/
|
|
449
|
+
_compareVersions(v1, v2) {
|
|
450
|
+
// Compare major.minor.patch first
|
|
451
|
+
if (v1.major !== v2.major) return v1.major - v2.major;
|
|
452
|
+
if (v1.minor !== v2.minor) return v1.minor - v2.minor;
|
|
453
|
+
if (v1.patch !== v2.patch) return v1.patch - v2.patch;
|
|
454
|
+
|
|
455
|
+
// When major.minor.patch are equal, check pre-release
|
|
456
|
+
// According to semver spec:
|
|
457
|
+
// 1. Release version (no prerelease) > prerelease version
|
|
458
|
+
// 2. If both have prerelease, compare identifiers
|
|
459
|
+
|
|
460
|
+
if (!v1.prerelease && !v2.prerelease) {
|
|
461
|
+
// Both are release versions, equal
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (!v1.prerelease && v2.prerelease) {
|
|
466
|
+
// v1 is release, v2 is pre-release → v1 > v2
|
|
467
|
+
return 1;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (v1.prerelease && !v2.prerelease) {
|
|
471
|
+
// v1 is pre-release, v2 is release → v1 < v2
|
|
472
|
+
return -1;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Both have pre-release, compare them
|
|
476
|
+
return this._comparePrerelease(v1.prerelease, v2.prerelease);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Compare pre-release version identifiers according to semver spec
|
|
481
|
+
* Identifiers are compared as:
|
|
482
|
+
* 1. Numeric identifiers are compared numerically
|
|
483
|
+
* 2. Alphanumeric identifiers are compared lexically (ASCII sort)
|
|
484
|
+
* 3. Numeric identifiers have lower precedence than alphanumeric
|
|
485
|
+
* 4. Larger set of identifiers has higher precedence if all preceding are equal
|
|
486
|
+
* @param {Array<string>} pre1 - First pre-release identifiers
|
|
487
|
+
* @param {Array<string>} pre2 - Second pre-release identifiers
|
|
488
|
+
* @returns {number} Comparison result
|
|
489
|
+
* @private
|
|
490
|
+
*/
|
|
491
|
+
_comparePrerelease(pre1, pre2) {
|
|
492
|
+
const len = Math.max(pre1.length, pre2.length);
|
|
493
|
+
|
|
494
|
+
for (let i = 0; i < len; i++) {
|
|
495
|
+
// If one pre-release has fewer identifiers, it has lower precedence
|
|
496
|
+
if (i >= pre1.length) return -1; // pre1 is shorter, pre1 < pre2
|
|
497
|
+
if (i >= pre2.length) return 1; // pre2 is shorter, pre1 > pre2
|
|
498
|
+
|
|
499
|
+
const part1 = pre1[i];
|
|
500
|
+
const part2 = pre2[i];
|
|
501
|
+
|
|
502
|
+
// Check if parts are numeric
|
|
503
|
+
const num1 = /^\d+$/.test(part1) ? parseInt(part1, 10) : null;
|
|
504
|
+
const num2 = /^\d+$/.test(part2) ? parseInt(part2, 10) : null;
|
|
505
|
+
|
|
506
|
+
// Both numeric: compare numerically
|
|
507
|
+
if (num1 !== null && num2 !== null) {
|
|
508
|
+
if (num1 !== num2) return num1 - num2;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// One numeric, one alphanumeric: numeric has lower precedence
|
|
513
|
+
if (num1 !== null && num2 === null) return -1;
|
|
514
|
+
if (num1 === null && num2 !== null) return 1;
|
|
515
|
+
|
|
516
|
+
// Both alphanumeric: compare lexically
|
|
517
|
+
if (part1 < part2) return -1;
|
|
518
|
+
if (part1 > part2) return 1;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// All identifiers equal
|
|
522
|
+
return 0;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Parse version range into components
|
|
527
|
+
* @param {string} range - Version range (e.g., "^4.17.1", "~4.17.1", "<5.0.0", "4.x", "4.*")
|
|
528
|
+
* @returns {Object|null} Parsed range or null
|
|
529
|
+
* @private
|
|
530
|
+
*/
|
|
531
|
+
_parseVersionRange(range) {
|
|
532
|
+
const trimmed = range.trim();
|
|
533
|
+
|
|
534
|
+
// Check for X-ranges first: 4.x, 4.*, 4.17.x, 4.17.*, or just 4
|
|
535
|
+
// X-ranges use 'x', '*', or missing parts as wildcards
|
|
536
|
+
const xRangePattern = /^(\d+|\*|x)(\.(\d+|\*|x))?(\.(\d+|\*|x))?$/i;
|
|
537
|
+
const xMatch = xRangePattern.exec(trimmed);
|
|
538
|
+
|
|
539
|
+
if (xMatch) {
|
|
540
|
+
const majorStr = xMatch[1];
|
|
541
|
+
const minorStr = xMatch[3];
|
|
542
|
+
const patchStr = xMatch[5];
|
|
543
|
+
|
|
544
|
+
// Check if any part is wildcard or missing (making it an X-range)
|
|
545
|
+
const isXRange =
|
|
546
|
+
majorStr === '*' || majorStr.toLowerCase() === 'x' ||
|
|
547
|
+
minorStr === undefined || minorStr === '*' || minorStr.toLowerCase() === 'x' ||
|
|
548
|
+
patchStr === undefined || patchStr === '*' || patchStr.toLowerCase() === 'x';
|
|
549
|
+
|
|
550
|
+
if (isXRange) {
|
|
551
|
+
return {
|
|
552
|
+
prefix: '',
|
|
553
|
+
operator: 'x-range',
|
|
554
|
+
major: (majorStr === '*' || majorStr.toLowerCase() === 'x') ? null : parseInt(majorStr, 10),
|
|
555
|
+
minor: (!minorStr || minorStr === '*' || minorStr.toLowerCase() === 'x') ? null : parseInt(minorStr, 10),
|
|
556
|
+
patch: (!patchStr || patchStr === '*' || patchStr.toLowerCase() === 'x') ? null : parseInt(patchStr, 10),
|
|
557
|
+
original: range
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Extract prefix and version for other operators
|
|
563
|
+
let prefix = '';
|
|
564
|
+
let version = trimmed;
|
|
565
|
+
let operator = '=';
|
|
566
|
+
|
|
567
|
+
if (trimmed.startsWith('^')) {
|
|
568
|
+
prefix = '^';
|
|
569
|
+
version = trimmed.slice(1);
|
|
570
|
+
operator = '^';
|
|
571
|
+
} else if (trimmed.startsWith('~')) {
|
|
572
|
+
prefix = '~';
|
|
573
|
+
version = trimmed.slice(1);
|
|
574
|
+
operator = '~';
|
|
575
|
+
} else if (trimmed.startsWith('>=')) {
|
|
576
|
+
prefix = '>=';
|
|
577
|
+
version = trimmed.slice(2).trim();
|
|
578
|
+
operator = '>=';
|
|
579
|
+
} else if (trimmed.startsWith('>')) {
|
|
580
|
+
prefix = '>';
|
|
581
|
+
version = trimmed.slice(1).trim();
|
|
582
|
+
operator = '>';
|
|
583
|
+
} else if (trimmed.startsWith('<=')) {
|
|
584
|
+
prefix = '<=';
|
|
585
|
+
version = trimmed.slice(2).trim();
|
|
586
|
+
operator = '<=';
|
|
587
|
+
} else if (trimmed.startsWith('<')) {
|
|
588
|
+
prefix = '<';
|
|
589
|
+
version = trimmed.slice(1).trim();
|
|
590
|
+
operator = '<';
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Parse version X.Y.Z (including pre-release and build)
|
|
594
|
+
const parsed = this._parseVersion(version);
|
|
595
|
+
|
|
596
|
+
if (!parsed) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
prefix,
|
|
602
|
+
operator,
|
|
603
|
+
major: parsed.major,
|
|
604
|
+
minor: parsed.minor,
|
|
605
|
+
patch: parsed.patch,
|
|
606
|
+
prerelease: parsed.prerelease,
|
|
607
|
+
build: parsed.build,
|
|
608
|
+
original: range
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Check if update is available and compatible with range
|
|
614
|
+
* @param {Object} rangeInfo - Parsed range information
|
|
615
|
+
* @param {Object} latest - Parsed latest version
|
|
616
|
+
* @returns {boolean} True if update available
|
|
617
|
+
* @private
|
|
618
|
+
*/
|
|
619
|
+
_isUpdateAvailable(rangeInfo, latest) {
|
|
620
|
+
const current = {
|
|
621
|
+
major: rangeInfo.major,
|
|
622
|
+
minor: rangeInfo.minor,
|
|
623
|
+
patch: rangeInfo.patch,
|
|
624
|
+
prerelease: rangeInfo.prerelease || null,
|
|
625
|
+
build: rangeInfo.build || null
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// Check based on operator
|
|
629
|
+
switch (rangeInfo.operator) {
|
|
630
|
+
case '^':
|
|
631
|
+
return this._isCaretUpdateAvailable(current, latest);
|
|
632
|
+
|
|
633
|
+
case '~':
|
|
634
|
+
return this._isTildeUpdateAvailable(current, latest);
|
|
635
|
+
|
|
636
|
+
case '>=':
|
|
637
|
+
case '>':
|
|
638
|
+
return this._isSimpleUpdateAvailable(current, latest);
|
|
639
|
+
|
|
640
|
+
case '<':
|
|
641
|
+
case '<=':
|
|
642
|
+
return this._isLessThanUpdateAvailable(rangeInfo, latest);
|
|
643
|
+
|
|
644
|
+
case 'x-range':
|
|
645
|
+
return this._isXRangeUpdateAvailable(rangeInfo, latest);
|
|
646
|
+
|
|
647
|
+
case '=':
|
|
648
|
+
default:
|
|
649
|
+
return this._isExactUpdateAvailable(current, latest);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Check if update is available for caret range (^)
|
|
655
|
+
* Handles special cases:
|
|
656
|
+
* - ^0.0.X → Only patch updates in 0.0.*
|
|
657
|
+
* - ^0.X.Y → Only patch updates in 0.X.*
|
|
658
|
+
* - ^X.Y.Z → Minor and patch updates in X.*.*
|
|
659
|
+
* Also handles pre-release versions correctly
|
|
660
|
+
* @private
|
|
661
|
+
*/
|
|
662
|
+
_isCaretUpdateAvailable(current, latest) {
|
|
663
|
+
// First check if latest is actually greater than current
|
|
664
|
+
const cmp = this._compareVersions(latest, current);
|
|
665
|
+
if (cmp <= 0) {
|
|
666
|
+
// latest is not greater than current
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ^0.0.X → Only patch updates in 0.0.*
|
|
671
|
+
if (current.major === 0 && current.minor === 0) {
|
|
672
|
+
return latest.major === 0 && latest.minor === 0;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ^0.X.Y → Only patch updates in 0.X.*
|
|
676
|
+
if (current.major === 0) {
|
|
677
|
+
return latest.major === 0 && latest.minor === current.minor;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ^X.Y.Z → Any minor/patch update in X.*.*
|
|
681
|
+
return latest.major === current.major;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Check if update is available for tilde range (~)
|
|
686
|
+
* ~X.Y.Z → Only patch updates in X.Y.*
|
|
687
|
+
* Also handles pre-release versions correctly
|
|
688
|
+
* @private
|
|
689
|
+
*/
|
|
690
|
+
_isTildeUpdateAvailable(current, latest) {
|
|
691
|
+
// First check if latest is actually greater than current
|
|
692
|
+
const cmp = this._compareVersions(latest, current);
|
|
693
|
+
if (cmp <= 0) {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Must be same major and minor
|
|
698
|
+
return latest.major === current.major && latest.minor === current.minor;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Check if update is available for simple comparison (>, >=)
|
|
703
|
+
* Also handles pre-release versions correctly
|
|
704
|
+
* @private
|
|
705
|
+
*/
|
|
706
|
+
_isSimpleUpdateAvailable(current, latest) {
|
|
707
|
+
// Use proper version comparison that handles pre-release
|
|
708
|
+
const cmp = this._compareVersions(latest, current);
|
|
709
|
+
return cmp > 0;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Check if update is available for exact version
|
|
714
|
+
* Suggests update if latest is newer
|
|
715
|
+
* @private
|
|
716
|
+
*/
|
|
717
|
+
_isExactUpdateAvailable(current, latest) {
|
|
718
|
+
return this._isSimpleUpdateAvailable(current, latest);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Check if update is available for less than operators (<, <=)
|
|
723
|
+
* <X.Y.Z → latest must be < range
|
|
724
|
+
* <=X.Y.Z → latest must be <= range
|
|
725
|
+
* Also handles pre-release versions correctly
|
|
726
|
+
* @param {Object} rangeInfo - Parsed range information
|
|
727
|
+
* @param {Object} latest - Parsed latest version
|
|
728
|
+
* @returns {boolean} True if update satisfies constraint
|
|
729
|
+
* @private
|
|
730
|
+
*/
|
|
731
|
+
_isLessThanUpdateAvailable(rangeInfo, latest) {
|
|
732
|
+
// Use proper version comparison that handles pre-release
|
|
733
|
+
const cmp = this._compareVersions(latest, rangeInfo);
|
|
734
|
+
|
|
735
|
+
if (rangeInfo.operator === '<') {
|
|
736
|
+
// latest must be < range
|
|
737
|
+
return cmp < 0;
|
|
738
|
+
} else if (rangeInfo.operator === '<=') {
|
|
739
|
+
// latest must be <= range
|
|
740
|
+
return cmp <= 0;
|
|
741
|
+
}
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Check if update is available for X-ranges (4.x, 4.*, 4.17.x, etc.)
|
|
747
|
+
* X-ranges match any version within the specified parts
|
|
748
|
+
* 4.x or 4.* → any version 4.Y.Z
|
|
749
|
+
* 4.17.x or 4.17.* → any version 4.17.Z
|
|
750
|
+
* *.*.* → any version
|
|
751
|
+
* @param {Object} rangeInfo - Parsed range information
|
|
752
|
+
* @param {Object} latest - Parsed latest version
|
|
753
|
+
* @returns {boolean} True if update is within range
|
|
754
|
+
* @private
|
|
755
|
+
*/
|
|
756
|
+
_isXRangeUpdateAvailable(rangeInfo, latest) {
|
|
757
|
+
// Check major version if specified
|
|
758
|
+
if (rangeInfo.major !== null && latest.major !== rangeInfo.major) {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Check minor version if specified
|
|
763
|
+
if (rangeInfo.minor !== null && latest.minor !== rangeInfo.minor) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Check patch version if specified
|
|
768
|
+
if (rangeInfo.patch !== null && latest.patch !== rangeInfo.patch) {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// All specified parts match, update is within range
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Check if a range string is a complex range (contains AND/OR operators)
|
|
778
|
+
* Complex ranges include:
|
|
779
|
+
* - OR ranges: "^4.0.0 || ^5.0.0"
|
|
780
|
+
* - AND ranges: ">=4.0.0 <5.0.0" (space-separated, multiple constraints)
|
|
781
|
+
* @param {string} range - Version range string
|
|
782
|
+
* @returns {boolean} True if complex range
|
|
783
|
+
* @private
|
|
784
|
+
*/
|
|
785
|
+
_isComplexRange(range) {
|
|
786
|
+
// Check for OR operator
|
|
787
|
+
if (range.includes('||')) {
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Check for AND (multiple space-separated ranges)
|
|
792
|
+
// Need to detect patterns like ">=4.0.0 <5.0.0"
|
|
793
|
+
// But NOT "^4.0.0" or "~4.0.0" (single ranges with spaces after)
|
|
794
|
+
const trimmed = range.trim();
|
|
795
|
+
|
|
796
|
+
// Remove single operator prefixes to see what's left
|
|
797
|
+
if (trimmed.startsWith('^') || trimmed.startsWith('~')) {
|
|
798
|
+
return false; // Single caret or tilde range
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Split by whitespace and check if there are multiple parts
|
|
802
|
+
const parts = trimmed.split(/\s+/).filter(p => p.length > 0);
|
|
803
|
+
|
|
804
|
+
// If we have multiple parts, it might be a complex AND range
|
|
805
|
+
// Examples: [">=4.0.0", "<5.0.0"], [">1.0.0", "<2.0.0"]
|
|
806
|
+
if (parts.length > 1) {
|
|
807
|
+
// Check if each part looks like a range operator
|
|
808
|
+
const rangeOperators = /^(>=?|<=?|\^|~|[0-9])/;
|
|
809
|
+
return parts.every(part => rangeOperators.test(part));
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Parse a complex range expression into a structured format
|
|
817
|
+
* Handles:
|
|
818
|
+
* - OR: "^4.0.0 || ^5.0.0" → {type: 'or', ranges: [...]}
|
|
819
|
+
* - AND: ">=4.0.0 <5.0.0" → {type: 'and', ranges: [...]}
|
|
820
|
+
* - Simple: "^4.0.0" → {type: 'simple', range: {...}}
|
|
821
|
+
* @param {string} range - Complex range string
|
|
822
|
+
* @returns {Object} Parsed complex range structure
|
|
823
|
+
* @private
|
|
824
|
+
*/
|
|
825
|
+
_parseComplexRange(range) {
|
|
826
|
+
// Split by OR first (|| has precedence)
|
|
827
|
+
if (range.includes('||')) {
|
|
828
|
+
const orParts = range.split('||').map(p => p.trim());
|
|
829
|
+
return {
|
|
830
|
+
type: 'or',
|
|
831
|
+
ranges: orParts.map(part => this._parseComplexRange(part))
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Split by AND (space-separated, no || present)
|
|
836
|
+
const andParts = range.trim().split(/\s+/).filter(p => p.length > 0);
|
|
837
|
+
|
|
838
|
+
if (andParts.length > 1) {
|
|
839
|
+
return {
|
|
840
|
+
type: 'and',
|
|
841
|
+
ranges: andParts.map(part => this._parseVersionRange(part))
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Simple range
|
|
846
|
+
return {
|
|
847
|
+
type: 'simple',
|
|
848
|
+
range: this._parseVersionRange(range)
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Check if a version satisfies a single range
|
|
854
|
+
* @param {Object} rangeInfo - Parsed range information
|
|
855
|
+
* @param {Object} version - Parsed version to check
|
|
856
|
+
* @returns {boolean} True if version satisfies range
|
|
857
|
+
* @private
|
|
858
|
+
*/
|
|
859
|
+
_satisfiesRange(rangeInfo, version) {
|
|
860
|
+
if (!rangeInfo || !version) {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Use the existing _isUpdateAvailable logic which handles all operators
|
|
865
|
+
return this._isUpdateAvailable(rangeInfo, version);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Check if a version satisfies a complex range expression
|
|
870
|
+
* Handles AND/OR logic recursively
|
|
871
|
+
* @param {Object} complexRange - Parsed complex range structure
|
|
872
|
+
* @param {Object} version - Parsed version to check
|
|
873
|
+
* @returns {boolean} True if version satisfies the complex range
|
|
874
|
+
* @private
|
|
875
|
+
*/
|
|
876
|
+
_satisfiesComplexRange(complexRange, version) {
|
|
877
|
+
if (!complexRange || !version) {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
switch (complexRange.type) {
|
|
882
|
+
case 'simple':
|
|
883
|
+
return this._satisfiesRange(complexRange.range, version);
|
|
884
|
+
|
|
885
|
+
case 'and':
|
|
886
|
+
// ALL ranges must be satisfied
|
|
887
|
+
return complexRange.ranges.every(range => this._satisfiesRange(range, version));
|
|
888
|
+
|
|
889
|
+
case 'or':
|
|
890
|
+
// AT LEAST ONE range must be satisfied
|
|
891
|
+
return complexRange.ranges.some(range => this._satisfiesComplexRange(range, version));
|
|
892
|
+
|
|
893
|
+
default:
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Check dependencies in batches to avoid overwhelming the registry
|
|
900
|
+
* @param {Object} dependencies - Map of package names to versions
|
|
901
|
+
* @returns {Promise<Object>} Map of packages with available updates
|
|
902
|
+
* @private
|
|
903
|
+
*/
|
|
904
|
+
async _checkDependencies(dependencies) {
|
|
905
|
+
const updates = {};
|
|
906
|
+
const entries = Object.entries(dependencies);
|
|
907
|
+
|
|
908
|
+
// Process in batches
|
|
909
|
+
for (let i = 0; i < entries.length; i += RESOLVER_CONFIG.MAX_CONCURRENT_CHECKS) {
|
|
910
|
+
const batch = entries.slice(i, i + RESOLVER_CONFIG.MAX_CONCURRENT_CHECKS);
|
|
911
|
+
|
|
912
|
+
const promises = batch.map(async ([pkg, range]) => {
|
|
913
|
+
try {
|
|
914
|
+
const latest = await this._getLatestCompatibleVersion(pkg, range);
|
|
915
|
+
|
|
916
|
+
if (latest) {
|
|
917
|
+
// Preserve the range prefix (^, ~, etc.)
|
|
918
|
+
const prefix = range.match(/^[\^~]/)?.[0] || '^';
|
|
919
|
+
return { pkg, newVersion: `${prefix}${latest}`, oldVersion: range };
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return null;
|
|
923
|
+
} catch (error) {
|
|
924
|
+
this.logger?.error(`Error checking ${pkg}:`, error.message);
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
const results = await Promise.all(promises);
|
|
930
|
+
|
|
931
|
+
results.forEach(result => {
|
|
932
|
+
if (result) {
|
|
933
|
+
updates[result.pkg] = result.newVersion;
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return updates;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Execute tool with parsed parameters
|
|
943
|
+
* @param {Object} params - Parsed parameters
|
|
944
|
+
* @param {Object} context - Execution context
|
|
945
|
+
* @returns {Promise<Object>} Execution result
|
|
946
|
+
*/
|
|
947
|
+
async execute(params, context = {}) {
|
|
948
|
+
try {
|
|
949
|
+
// Validate parameters
|
|
950
|
+
this._validateParameters(params);
|
|
951
|
+
|
|
952
|
+
const { path: targetPath, mode, includeDev } = params;
|
|
953
|
+
const { projectDir, agentId, directoryAccess } = context;
|
|
954
|
+
|
|
955
|
+
// Resolve and validate path
|
|
956
|
+
const resolvedPath = this._resolveAndValidatePath(targetPath, context);
|
|
957
|
+
const pkgPath = path.join(resolvedPath, 'package.json');
|
|
958
|
+
|
|
959
|
+
this.logger?.info('Dependency resolver executing', {
|
|
960
|
+
mode,
|
|
961
|
+
resolvedPath,
|
|
962
|
+
includeDev,
|
|
963
|
+
agentId
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
const output = [];
|
|
967
|
+
output.push(`🔍 Checking dependencies in: ${resolvedPath}`);
|
|
968
|
+
output.push(`Mode: ${mode}`);
|
|
969
|
+
|
|
970
|
+
// Check if package.json exists
|
|
971
|
+
try {
|
|
972
|
+
await fs.access(pkgPath);
|
|
973
|
+
} catch {
|
|
974
|
+
return {
|
|
975
|
+
success: false,
|
|
976
|
+
error: `No package.json found at: ${pkgPath}`,
|
|
977
|
+
output: output.join('\n')
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Read package.json
|
|
982
|
+
output.push('\n📦 Reading package.json...');
|
|
983
|
+
const pkgContent = await fs.readFile(pkgPath, 'utf-8');
|
|
984
|
+
const pkgData = JSON.parse(pkgContent);
|
|
985
|
+
|
|
986
|
+
// Collect dependencies
|
|
987
|
+
const allDeps = { ...pkgData.dependencies };
|
|
988
|
+
|
|
989
|
+
if (includeDev && pkgData.devDependencies) {
|
|
990
|
+
Object.assign(allDeps, pkgData.devDependencies);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (Object.keys(allDeps).length === 0) {
|
|
994
|
+
return {
|
|
995
|
+
success: true,
|
|
996
|
+
mode,
|
|
997
|
+
message: 'No dependencies found in package.json',
|
|
998
|
+
statistics: {
|
|
999
|
+
totalDependencies: 0,
|
|
1000
|
+
updatesAvailable: 0,
|
|
1001
|
+
updatesApplied: 0,
|
|
1002
|
+
errors: 0
|
|
1003
|
+
},
|
|
1004
|
+
output: output.join('\n')
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Safety check
|
|
1009
|
+
if (Object.keys(allDeps).length > RESOLVER_CONFIG.MAX_DEPENDENCIES) {
|
|
1010
|
+
return {
|
|
1011
|
+
success: false,
|
|
1012
|
+
error: `Too many dependencies (${Object.keys(allDeps).length}), max allowed: ${RESOLVER_CONFIG.MAX_DEPENDENCIES}`,
|
|
1013
|
+
output: output.join('\n')
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
output.push(`📊 Found ${Object.keys(allDeps).length} dependencies to check`);
|
|
1018
|
+
output.push('🌐 Querying npm registry for updates...');
|
|
1019
|
+
|
|
1020
|
+
// Check for updates
|
|
1021
|
+
const updates = await this._checkDependencies(allDeps);
|
|
1022
|
+
|
|
1023
|
+
output.push(`\n✅ Registry check complete`);
|
|
1024
|
+
output.push(`📈 Updates available: ${Object.keys(updates).length}`);
|
|
1025
|
+
|
|
1026
|
+
if (Object.keys(updates).length > 0) {
|
|
1027
|
+
output.push('\n📋 Available updates:');
|
|
1028
|
+
for (const [pkg, newVersion] of Object.entries(updates)) {
|
|
1029
|
+
const oldVersion = allDeps[pkg];
|
|
1030
|
+
output.push(` • ${pkg}: ${oldVersion} → ${newVersion}`);
|
|
1031
|
+
}
|
|
1032
|
+
} else {
|
|
1033
|
+
output.push('\n🎉 All dependencies are up-to-date!');
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Handle modes
|
|
1037
|
+
if (mode === 'check') {
|
|
1038
|
+
// Check mode - just report
|
|
1039
|
+
if (Object.keys(updates).length > 0) {
|
|
1040
|
+
output.push('\n💡 Run with mode="fix" or mode="auto" to apply updates');
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
return {
|
|
1044
|
+
success: true,
|
|
1045
|
+
mode: 'check',
|
|
1046
|
+
message: `Found ${Object.keys(updates).length} package(s) with available updates`,
|
|
1047
|
+
statistics: {
|
|
1048
|
+
totalDependencies: Object.keys(allDeps).length,
|
|
1049
|
+
updatesAvailable: Object.keys(updates).length,
|
|
1050
|
+
updatesApplied: 0,
|
|
1051
|
+
errors: 0
|
|
1052
|
+
},
|
|
1053
|
+
updates,
|
|
1054
|
+
output: output.join('\n')
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Fix or auto mode - apply updates
|
|
1059
|
+
if ((mode === 'fix' || mode === 'auto') && Object.keys(updates).length > 0) {
|
|
1060
|
+
// Create backup
|
|
1061
|
+
output.push('\n💾 Creating backup of package.json...');
|
|
1062
|
+
const backupPath = await this._createBackup(pkgPath);
|
|
1063
|
+
|
|
1064
|
+
if (backupPath) {
|
|
1065
|
+
output.push(`✅ Backup created: ${path.basename(backupPath)}`);
|
|
1066
|
+
} else {
|
|
1067
|
+
output.push('⚠️ Backup creation failed, continuing anyway...');
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Update package.json
|
|
1071
|
+
output.push('\n🛠 Updating package.json...');
|
|
1072
|
+
|
|
1073
|
+
for (const [pkg, newVersion] of Object.entries(updates)) {
|
|
1074
|
+
if (pkgData.dependencies?.[pkg]) {
|
|
1075
|
+
pkgData.dependencies[pkg] = newVersion;
|
|
1076
|
+
}
|
|
1077
|
+
if (pkgData.devDependencies?.[pkg]) {
|
|
1078
|
+
pkgData.devDependencies[pkg] = newVersion;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Write updated package.json
|
|
1083
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkgData, null, 2) + '\n');
|
|
1084
|
+
output.push('✅ package.json updated');
|
|
1085
|
+
|
|
1086
|
+
// Run npm install
|
|
1087
|
+
output.push('\n📥 Installing dependencies (this may take a while)...');
|
|
1088
|
+
|
|
1089
|
+
try {
|
|
1090
|
+
const { stdout, stderr } = await exec('npm install', {
|
|
1091
|
+
cwd: resolvedPath,
|
|
1092
|
+
timeout: RESOLVER_CONFIG.NPM_COMMAND_TIMEOUT,
|
|
1093
|
+
maxBuffer: 1024 * 1024 * 10 // 10MB buffer
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
if (stdout) {
|
|
1097
|
+
// Only show summary, not full output
|
|
1098
|
+
const lines = stdout.trim().split('\n');
|
|
1099
|
+
if (lines.length > 10) {
|
|
1100
|
+
output.push(' ' + lines.slice(-5).join('\n '));
|
|
1101
|
+
} else {
|
|
1102
|
+
output.push(' ' + stdout.trim());
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (stderr && !stderr.includes('npm WARN')) {
|
|
1107
|
+
output.push(`⚠️ ${stderr.trim()}`);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
output.push('✅ Installation complete');
|
|
1111
|
+
|
|
1112
|
+
// Show installed versions
|
|
1113
|
+
try {
|
|
1114
|
+
const { stdout: lsOutput } = await exec('npm ls --depth=0 --json', {
|
|
1115
|
+
cwd: resolvedPath,
|
|
1116
|
+
timeout: 30000
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
const lsData = JSON.parse(lsOutput);
|
|
1120
|
+
|
|
1121
|
+
if (lsData.dependencies) {
|
|
1122
|
+
output.push('\n📦 Installed versions:');
|
|
1123
|
+
for (const pkg of Object.keys(updates)) {
|
|
1124
|
+
if (lsData.dependencies[pkg]) {
|
|
1125
|
+
output.push(` • ${pkg}@${lsData.dependencies[pkg].version}`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
} catch (lsError) {
|
|
1130
|
+
// npm ls might fail with peer dependency warnings - that's okay
|
|
1131
|
+
this.logger?.debug('npm ls failed:', lsError.message);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
return {
|
|
1135
|
+
success: true,
|
|
1136
|
+
mode,
|
|
1137
|
+
message: `Successfully updated ${Object.keys(updates).length} package(s)`,
|
|
1138
|
+
statistics: {
|
|
1139
|
+
totalDependencies: Object.keys(allDeps).length,
|
|
1140
|
+
updatesAvailable: Object.keys(updates).length,
|
|
1141
|
+
updatesApplied: Object.keys(updates).length,
|
|
1142
|
+
errors: 0
|
|
1143
|
+
},
|
|
1144
|
+
updates,
|
|
1145
|
+
backupCreated: backupPath !== null,
|
|
1146
|
+
backupPath,
|
|
1147
|
+
output: output.join('\n')
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
} catch (installError) {
|
|
1151
|
+
output.push(`\n❌ npm install failed: ${installError.message}`);
|
|
1152
|
+
|
|
1153
|
+
// Try to restore from backup
|
|
1154
|
+
if (backupPath) {
|
|
1155
|
+
try {
|
|
1156
|
+
await fs.copyFile(backupPath, pkgPath);
|
|
1157
|
+
output.push('🔄 Restored package.json from backup');
|
|
1158
|
+
} catch (restoreError) {
|
|
1159
|
+
output.push('⚠️ Failed to restore backup');
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return {
|
|
1164
|
+
success: false,
|
|
1165
|
+
mode,
|
|
1166
|
+
error: `npm install failed: ${installError.message}`,
|
|
1167
|
+
statistics: {
|
|
1168
|
+
totalDependencies: Object.keys(allDeps).length,
|
|
1169
|
+
updatesAvailable: Object.keys(updates).length,
|
|
1170
|
+
updatesApplied: 0,
|
|
1171
|
+
errors: 1
|
|
1172
|
+
},
|
|
1173
|
+
updates,
|
|
1174
|
+
backupCreated: backupPath !== null,
|
|
1175
|
+
output: output.join('\n')
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
} else if (mode === 'fix' || mode === 'auto') {
|
|
1179
|
+
// No updates needed
|
|
1180
|
+
output.push('\n✨ No updates needed');
|
|
1181
|
+
|
|
1182
|
+
return {
|
|
1183
|
+
success: true,
|
|
1184
|
+
mode,
|
|
1185
|
+
message: 'All dependencies are up-to-date',
|
|
1186
|
+
statistics: {
|
|
1187
|
+
totalDependencies: Object.keys(allDeps).length,
|
|
1188
|
+
updatesAvailable: 0,
|
|
1189
|
+
updatesApplied: 0,
|
|
1190
|
+
errors: 0
|
|
1191
|
+
},
|
|
1192
|
+
updates: {},
|
|
1193
|
+
output: output.join('\n')
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
this.logger?.error('Dependency resolver error:', error);
|
|
1199
|
+
|
|
1200
|
+
return {
|
|
1201
|
+
success: false,
|
|
1202
|
+
error: error.message,
|
|
1203
|
+
statistics: {
|
|
1204
|
+
totalDependencies: 0,
|
|
1205
|
+
updatesAvailable: 0,
|
|
1206
|
+
updatesApplied: 0,
|
|
1207
|
+
errors: 1
|
|
1208
|
+
},
|
|
1209
|
+
output: error.message
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
export default DependencyResolverTool;
|