@deplens/mcp 0.1.6 → 0.1.8
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/bin/deplens-mcp.js +0 -0
- package/package.json +6 -3
- package/src/core/changelog-parser.mjs +313 -0
- package/src/core/diff-analyzer.mjs +590 -0
- package/src/core/diff.mjs +145 -0
- package/src/core/inspect.mjs +950 -482
- package/src/core/parse-dts.mjs +198 -172
- package/src/core/parse-source.mjs +524 -0
- package/src/core/version-resolver.mjs +317 -0
- package/src/server.mjs +579 -40
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diff-analyzer.mjs - Semantic comparison of package versions
|
|
3
|
+
* Detects breaking changes, additions, and modifications
|
|
4
|
+
* Self-contained: no dependencies on inspect.mjs or fast-glob
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import ts from "typescript";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Change types and their severity
|
|
13
|
+
*/
|
|
14
|
+
export const ChangeType = {
|
|
15
|
+
REMOVED: "removed", // BREAKING
|
|
16
|
+
SIGNATURE_CHANGED: "changed", // Potentially BREAKING
|
|
17
|
+
ADDED: "added", // Safe
|
|
18
|
+
DEPRECATED: "deprecated", // Warning
|
|
19
|
+
COMPLEXITY_INCREASED: "complexity", // Info
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Severity = {
|
|
23
|
+
BREAKING: "breaking",
|
|
24
|
+
WARNING: "warning",
|
|
25
|
+
INFO: "info",
|
|
26
|
+
SAFE: "safe",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Normalize type string for comparison
|
|
31
|
+
*/
|
|
32
|
+
function normalizeType(type) {
|
|
33
|
+
if (!type) return "";
|
|
34
|
+
return type
|
|
35
|
+
.replace(/\s+/g, " ")
|
|
36
|
+
.replace(/\s*([,:<>{}()[\]|&])\s*/g, "$1")
|
|
37
|
+
.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Compare function signatures
|
|
42
|
+
*/
|
|
43
|
+
function compareFunctionSignatures(from, to) {
|
|
44
|
+
const changes = [];
|
|
45
|
+
|
|
46
|
+
// Compare parameters
|
|
47
|
+
const fromParams = from.params || [];
|
|
48
|
+
const toParams = to.params || [];
|
|
49
|
+
|
|
50
|
+
// Check for removed required params (BREAKING)
|
|
51
|
+
for (let i = 0; i < fromParams.length; i++) {
|
|
52
|
+
const fromParam = fromParams[i];
|
|
53
|
+
const toParam = toParams[i];
|
|
54
|
+
|
|
55
|
+
if (!toParam) {
|
|
56
|
+
// Parameter removed
|
|
57
|
+
if (!fromParam.optional) {
|
|
58
|
+
changes.push({
|
|
59
|
+
type: "param_removed",
|
|
60
|
+
severity: Severity.BREAKING,
|
|
61
|
+
detail: `Required parameter '${fromParam.name}' removed`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
} else if (normalizeType(fromParam.type) !== normalizeType(toParam.type)) {
|
|
65
|
+
changes.push({
|
|
66
|
+
type: "param_type_changed",
|
|
67
|
+
severity: Severity.WARNING,
|
|
68
|
+
detail: `Parameter '${fromParam.name}' type: ${fromParam.type} → ${toParam.type}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check for new required params (BREAKING)
|
|
74
|
+
for (let i = fromParams.length; i < toParams.length; i++) {
|
|
75
|
+
const toParam = toParams[i];
|
|
76
|
+
if (!toParam.optional && !toParam.default) {
|
|
77
|
+
changes.push({
|
|
78
|
+
type: "param_added_required",
|
|
79
|
+
severity: Severity.BREAKING,
|
|
80
|
+
detail: `New required parameter '${toParam.name}: ${toParam.type}'`,
|
|
81
|
+
});
|
|
82
|
+
} else {
|
|
83
|
+
changes.push({
|
|
84
|
+
type: "param_added_optional",
|
|
85
|
+
severity: Severity.SAFE,
|
|
86
|
+
detail: `New optional parameter '${toParam.name}?: ${toParam.type}'`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Compare return types
|
|
92
|
+
const fromReturn = normalizeType(from.returnType || from.type);
|
|
93
|
+
const toReturn = normalizeType(to.returnType || to.type);
|
|
94
|
+
|
|
95
|
+
if (fromReturn && toReturn && fromReturn !== toReturn) {
|
|
96
|
+
// Check if return type was narrowed (safe) or widened (potentially breaking)
|
|
97
|
+
changes.push({
|
|
98
|
+
type: "return_type_changed",
|
|
99
|
+
severity: Severity.WARNING,
|
|
100
|
+
detail: `Return type: ${fromReturn} → ${toReturn}`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return changes;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Compare interface/type properties
|
|
109
|
+
*/
|
|
110
|
+
function compareProperties(fromProps, toProps) {
|
|
111
|
+
const changes = [];
|
|
112
|
+
const fromMap = new Map(Object.entries(fromProps || {}));
|
|
113
|
+
const toMap = new Map(Object.entries(toProps || {}));
|
|
114
|
+
|
|
115
|
+
// Check removed properties
|
|
116
|
+
for (const [name, prop] of fromMap) {
|
|
117
|
+
if (!toMap.has(name)) {
|
|
118
|
+
changes.push({
|
|
119
|
+
type: "property_removed",
|
|
120
|
+
severity: prop.optional ? Severity.SAFE : Severity.BREAKING,
|
|
121
|
+
detail: `Property '${name}' removed`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check added/changed properties
|
|
127
|
+
for (const [name, toProp] of toMap) {
|
|
128
|
+
const fromProp = fromMap.get(name);
|
|
129
|
+
if (!fromProp) {
|
|
130
|
+
changes.push({
|
|
131
|
+
type: "property_added",
|
|
132
|
+
severity: toProp.optional ? Severity.SAFE : Severity.BREAKING,
|
|
133
|
+
detail: `Property '${name}${toProp.optional ? "?" : ""}: ${toProp.type}' added`,
|
|
134
|
+
});
|
|
135
|
+
} else if (normalizeType(fromProp.type) !== normalizeType(toProp.type)) {
|
|
136
|
+
changes.push({
|
|
137
|
+
type: "property_type_changed",
|
|
138
|
+
severity: Severity.WARNING,
|
|
139
|
+
detail: `Property '${name}' type: ${fromProp.type} → ${toProp.type}`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return changes;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Analyze types from a package directory - self-contained implementation
|
|
149
|
+
*/
|
|
150
|
+
async function analyzePackageTypes(packageDir, options = {}) {
|
|
151
|
+
const pkgJsonPath = path.join(packageDir, "package.json");
|
|
152
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
153
|
+
return { error: "package.json not found", exports: {} };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
157
|
+
|
|
158
|
+
const result = {
|
|
159
|
+
version: pkg.version,
|
|
160
|
+
name: pkg.name,
|
|
161
|
+
exports: {
|
|
162
|
+
functions: {},
|
|
163
|
+
classes: {},
|
|
164
|
+
interfaces: {},
|
|
165
|
+
types: {},
|
|
166
|
+
enums: {},
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Find and parse all .d.ts files, following re-exports
|
|
171
|
+
const visited = new Set();
|
|
172
|
+
const allExports = {
|
|
173
|
+
functions: {},
|
|
174
|
+
classes: {},
|
|
175
|
+
interfaces: {},
|
|
176
|
+
types: {},
|
|
177
|
+
enums: {},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Start from the main types entry point
|
|
181
|
+
const entryPoints = [
|
|
182
|
+
pkg.types,
|
|
183
|
+
pkg.typings,
|
|
184
|
+
"index.d.ts",
|
|
185
|
+
"lib/index.d.ts",
|
|
186
|
+
"dist/index.d.ts",
|
|
187
|
+
].filter(Boolean);
|
|
188
|
+
|
|
189
|
+
for (const entry of entryPoints) {
|
|
190
|
+
const fullPath = path.join(packageDir, entry);
|
|
191
|
+
if (fs.existsSync(fullPath)) {
|
|
192
|
+
parseTypesRecursively(fullPath, packageDir, allExports, visited);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
result.exports = allExports;
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Parse a .d.ts file and follow re-exports recursively
|
|
203
|
+
*/
|
|
204
|
+
function parseTypesRecursively(filePath, baseDir, allExports, visited) {
|
|
205
|
+
if (visited.has(filePath) || !fs.existsSync(filePath)) return;
|
|
206
|
+
visited.add(filePath);
|
|
207
|
+
|
|
208
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
209
|
+
const sourceFile = ts.createSourceFile(
|
|
210
|
+
path.basename(filePath),
|
|
211
|
+
content,
|
|
212
|
+
ts.ScriptTarget.Latest,
|
|
213
|
+
true,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const fileDir = path.dirname(filePath);
|
|
217
|
+
|
|
218
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
219
|
+
// Handle export declarations: export { x } from './module'
|
|
220
|
+
if (ts.isExportDeclaration(node)) {
|
|
221
|
+
if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
222
|
+
const modulePath = node.moduleSpecifier.text;
|
|
223
|
+
const resolvedPath = resolveModulePath(modulePath, fileDir, baseDir);
|
|
224
|
+
if (resolvedPath) {
|
|
225
|
+
parseTypesRecursively(resolvedPath, baseDir, allExports, visited);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Handle function declarations
|
|
231
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
232
|
+
const name = node.name.text;
|
|
233
|
+
const params =
|
|
234
|
+
node.parameters?.map((p) => ({
|
|
235
|
+
name: p.name.getText(sourceFile),
|
|
236
|
+
type: p.type ? p.type.getText(sourceFile) : "any",
|
|
237
|
+
optional: !!p.questionToken,
|
|
238
|
+
})) || [];
|
|
239
|
+
const returnType = node.type ? node.type.getText(sourceFile) : "void";
|
|
240
|
+
allExports.functions[name] = { params, returnType };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Handle class declarations
|
|
244
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
245
|
+
const name = node.name.text;
|
|
246
|
+
const methods = {};
|
|
247
|
+
node.members?.forEach((member) => {
|
|
248
|
+
if (ts.isMethodDeclaration(member) && member.name) {
|
|
249
|
+
const methodName = member.name.getText(sourceFile);
|
|
250
|
+
const params =
|
|
251
|
+
member.parameters?.map((p) => ({
|
|
252
|
+
name: p.name.getText(sourceFile),
|
|
253
|
+
type: p.type ? p.type.getText(sourceFile) : "any",
|
|
254
|
+
optional: !!p.questionToken,
|
|
255
|
+
})) || [];
|
|
256
|
+
const returnType = member.type
|
|
257
|
+
? member.type.getText(sourceFile)
|
|
258
|
+
: "void";
|
|
259
|
+
methods[methodName] = { params, returnType };
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
allExports.classes[name] = { methods };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Handle interface declarations
|
|
266
|
+
if (ts.isInterfaceDeclaration(node) && node.name) {
|
|
267
|
+
const name = node.name.text;
|
|
268
|
+
const properties = {};
|
|
269
|
+
node.members?.forEach((member) => {
|
|
270
|
+
if (ts.isPropertySignature(member) && member.name) {
|
|
271
|
+
const propName = member.name.getText(sourceFile);
|
|
272
|
+
properties[propName] = {
|
|
273
|
+
type: member.type ? member.type.getText(sourceFile) : "any",
|
|
274
|
+
optional: !!member.questionToken,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
allExports.interfaces[name] = { properties };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Handle type aliases
|
|
282
|
+
if (ts.isTypeAliasDeclaration(node) && node.name) {
|
|
283
|
+
const name = node.name.text;
|
|
284
|
+
allExports.types[name] = {
|
|
285
|
+
type: node.type ? node.type.getText(sourceFile) : "unknown",
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Handle enum declarations
|
|
290
|
+
if (ts.isEnumDeclaration(node) && node.name) {
|
|
291
|
+
const name = node.name.text;
|
|
292
|
+
const members =
|
|
293
|
+
node.members?.map((m) => m.name.getText(sourceFile)) || [];
|
|
294
|
+
allExports.enums[name] = { members };
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Resolve a module path to an actual file path
|
|
301
|
+
*/
|
|
302
|
+
function resolveModulePath(modulePath, fromDir, baseDir) {
|
|
303
|
+
// Handle relative paths
|
|
304
|
+
if (modulePath.startsWith(".")) {
|
|
305
|
+
const candidates = [
|
|
306
|
+
path.join(fromDir, modulePath + ".d.ts"),
|
|
307
|
+
path.join(fromDir, modulePath, "index.d.ts"),
|
|
308
|
+
path.join(fromDir, modulePath + ".ts"),
|
|
309
|
+
path.join(fromDir, modulePath),
|
|
310
|
+
];
|
|
311
|
+
for (const candidate of candidates) {
|
|
312
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Compare two package versions
|
|
320
|
+
*/
|
|
321
|
+
export async function compareVersions(fromDir, toDir, options = {}) {
|
|
322
|
+
const { filter, includeSource = false } = options;
|
|
323
|
+
|
|
324
|
+
// Analyze both versions
|
|
325
|
+
const [fromAnalysis, toAnalysis] = await Promise.all([
|
|
326
|
+
analyzePackageTypes(fromDir, { filter, includeSource }),
|
|
327
|
+
analyzePackageTypes(toDir, { filter, includeSource }),
|
|
328
|
+
]);
|
|
329
|
+
|
|
330
|
+
const diff = {
|
|
331
|
+
from: {
|
|
332
|
+
version: fromAnalysis.version,
|
|
333
|
+
name: fromAnalysis.name,
|
|
334
|
+
},
|
|
335
|
+
to: {
|
|
336
|
+
version: toAnalysis.version,
|
|
337
|
+
name: toAnalysis.name,
|
|
338
|
+
},
|
|
339
|
+
breaking: [],
|
|
340
|
+
warnings: [],
|
|
341
|
+
additions: [],
|
|
342
|
+
info: [],
|
|
343
|
+
summary: {
|
|
344
|
+
breaking: 0,
|
|
345
|
+
warnings: 0,
|
|
346
|
+
additions: 0,
|
|
347
|
+
removals: 0,
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Compare each export category
|
|
352
|
+
const categories = ["functions", "classes", "interfaces", "types", "enums"];
|
|
353
|
+
|
|
354
|
+
for (const category of categories) {
|
|
355
|
+
const fromExports = fromAnalysis.exports[category] || {};
|
|
356
|
+
const toExports = toAnalysis.exports[category] || {};
|
|
357
|
+
|
|
358
|
+
// Find removed exports (BREAKING)
|
|
359
|
+
for (const [name, fromItem] of Object.entries(fromExports)) {
|
|
360
|
+
if (!(name in toExports)) {
|
|
361
|
+
diff.breaking.push({
|
|
362
|
+
category,
|
|
363
|
+
name,
|
|
364
|
+
type: ChangeType.REMOVED,
|
|
365
|
+
severity: Severity.BREAKING,
|
|
366
|
+
detail: `${category.slice(0, -1)} '${name}' was removed`,
|
|
367
|
+
from: fromItem,
|
|
368
|
+
to: null,
|
|
369
|
+
});
|
|
370
|
+
diff.summary.breaking++;
|
|
371
|
+
diff.summary.removals++;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Find added exports
|
|
376
|
+
for (const [name, toItem] of Object.entries(toExports)) {
|
|
377
|
+
if (!(name in fromExports)) {
|
|
378
|
+
diff.additions.push({
|
|
379
|
+
category,
|
|
380
|
+
name,
|
|
381
|
+
type: ChangeType.ADDED,
|
|
382
|
+
severity: Severity.SAFE,
|
|
383
|
+
detail: `${category.slice(0, -1)} '${name}' was added`,
|
|
384
|
+
from: null,
|
|
385
|
+
to: toItem,
|
|
386
|
+
});
|
|
387
|
+
diff.summary.additions++;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Find changed exports
|
|
392
|
+
for (const [name, fromItem] of Object.entries(fromExports)) {
|
|
393
|
+
const toItem = toExports[name];
|
|
394
|
+
if (!toItem) continue;
|
|
395
|
+
|
|
396
|
+
let changes = [];
|
|
397
|
+
|
|
398
|
+
if (category === "functions") {
|
|
399
|
+
changes = compareFunctionSignatures(fromItem, toItem);
|
|
400
|
+
} else if (category === "interfaces" || category === "types") {
|
|
401
|
+
if (fromItem.properties && toItem.properties) {
|
|
402
|
+
changes = compareProperties(fromItem.properties, toItem.properties);
|
|
403
|
+
}
|
|
404
|
+
} else if (category === "classes") {
|
|
405
|
+
// Compare class methods and properties
|
|
406
|
+
if (fromItem.methods && toItem.methods) {
|
|
407
|
+
for (const [methodName, fromMethod] of Object.entries(
|
|
408
|
+
fromItem.methods,
|
|
409
|
+
)) {
|
|
410
|
+
const toMethod = toItem.methods[methodName];
|
|
411
|
+
if (!toMethod) {
|
|
412
|
+
changes.push({
|
|
413
|
+
type: "method_removed",
|
|
414
|
+
severity: Severity.BREAKING,
|
|
415
|
+
detail: `Method '${methodName}' removed from class '${name}'`,
|
|
416
|
+
});
|
|
417
|
+
} else {
|
|
418
|
+
const methodChanges = compareFunctionSignatures(
|
|
419
|
+
fromMethod,
|
|
420
|
+
toMethod,
|
|
421
|
+
);
|
|
422
|
+
changes.push(
|
|
423
|
+
...methodChanges.map((c) => ({
|
|
424
|
+
...c,
|
|
425
|
+
detail: `${name}.${methodName}: ${c.detail}`,
|
|
426
|
+
})),
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Check for new methods
|
|
431
|
+
for (const methodName of Object.keys(toItem.methods || {})) {
|
|
432
|
+
if (!fromItem.methods?.[methodName]) {
|
|
433
|
+
changes.push({
|
|
434
|
+
type: "method_added",
|
|
435
|
+
severity: Severity.SAFE,
|
|
436
|
+
detail: `Method '${methodName}' added to class '${name}'`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Categorize changes by severity
|
|
444
|
+
for (const change of changes) {
|
|
445
|
+
const entry = {
|
|
446
|
+
category,
|
|
447
|
+
name,
|
|
448
|
+
...change,
|
|
449
|
+
from: fromItem,
|
|
450
|
+
to: toItem,
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
if (change.severity === Severity.BREAKING) {
|
|
454
|
+
diff.breaking.push(entry);
|
|
455
|
+
diff.summary.breaking++;
|
|
456
|
+
} else if (change.severity === Severity.WARNING) {
|
|
457
|
+
diff.warnings.push(entry);
|
|
458
|
+
diff.summary.warnings++;
|
|
459
|
+
} else {
|
|
460
|
+
diff.info.push(entry);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Compare source complexity if available
|
|
467
|
+
if (
|
|
468
|
+
includeSource &&
|
|
469
|
+
fromAnalysis.sourceAnalysis &&
|
|
470
|
+
toAnalysis.sourceAnalysis
|
|
471
|
+
) {
|
|
472
|
+
const fromAvg = fromAnalysis.sourceAnalysis.summary?.avgComplexity || 0;
|
|
473
|
+
const toAvg = toAnalysis.sourceAnalysis.summary?.avgComplexity || 0;
|
|
474
|
+
|
|
475
|
+
if (toAvg > fromAvg * 1.5) {
|
|
476
|
+
// 50% increase
|
|
477
|
+
diff.warnings.push({
|
|
478
|
+
category: "source",
|
|
479
|
+
name: "complexity",
|
|
480
|
+
type: ChangeType.COMPLEXITY_INCREASED,
|
|
481
|
+
severity: Severity.WARNING,
|
|
482
|
+
detail: `Average complexity increased significantly: ${fromAvg} → ${toAvg}`,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
diff.sourceComparison = {
|
|
487
|
+
from: fromAnalysis.sourceAnalysis.summary,
|
|
488
|
+
to: toAnalysis.sourceAnalysis.summary,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return diff;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Format diff result as text
|
|
497
|
+
*/
|
|
498
|
+
export function formatDiffAsText(diff, options = {}) {
|
|
499
|
+
const { colors = true, verbose = false } = options;
|
|
500
|
+
|
|
501
|
+
const lines = [];
|
|
502
|
+
const red = colors ? "\x1b[31m" : "";
|
|
503
|
+
const green = colors ? "\x1b[32m" : "";
|
|
504
|
+
const yellow = colors ? "\x1b[33m" : "";
|
|
505
|
+
const reset = colors ? "\x1b[0m" : "";
|
|
506
|
+
const bold = colors ? "\x1b[1m" : "";
|
|
507
|
+
|
|
508
|
+
lines.push(
|
|
509
|
+
`${bold}📦 ${diff.from.name}: ${diff.from.version} → ${diff.to.version}${reset}`,
|
|
510
|
+
);
|
|
511
|
+
lines.push("");
|
|
512
|
+
|
|
513
|
+
// Breaking changes
|
|
514
|
+
if (diff.breaking.length > 0) {
|
|
515
|
+
lines.push(
|
|
516
|
+
`${red}${bold}🔴 BREAKING CHANGES (${diff.breaking.length}):${reset}`,
|
|
517
|
+
);
|
|
518
|
+
for (const change of diff.breaking) {
|
|
519
|
+
lines.push(`${red} - ${change.detail}${reset}`);
|
|
520
|
+
}
|
|
521
|
+
lines.push("");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Warnings
|
|
525
|
+
if (diff.warnings.length > 0) {
|
|
526
|
+
lines.push(
|
|
527
|
+
`${yellow}${bold}🟡 WARNINGS (${diff.warnings.length}):${reset}`,
|
|
528
|
+
);
|
|
529
|
+
for (const change of diff.warnings) {
|
|
530
|
+
lines.push(`${yellow} ~ ${change.detail}${reset}`);
|
|
531
|
+
}
|
|
532
|
+
lines.push("");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Additions
|
|
536
|
+
if (diff.additions.length > 0) {
|
|
537
|
+
lines.push(`${green}${bold}🟢 ADDED (${diff.additions.length}):${reset}`);
|
|
538
|
+
for (const change of diff.additions) {
|
|
539
|
+
lines.push(
|
|
540
|
+
`${green} + ${change.name} (${change.category.slice(0, -1)})${reset}`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
lines.push("");
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Info (only in verbose mode)
|
|
547
|
+
if (verbose && diff.info.length > 0) {
|
|
548
|
+
lines.push(`ℹ️ INFO (${diff.info.length}):`);
|
|
549
|
+
for (const change of diff.info) {
|
|
550
|
+
lines.push(` · ${change.detail}`);
|
|
551
|
+
}
|
|
552
|
+
lines.push("");
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Summary
|
|
556
|
+
lines.push(`${bold}📊 Summary:${reset}`);
|
|
557
|
+
lines.push(` Breaking: ${diff.summary.breaking}`);
|
|
558
|
+
lines.push(` Warnings: ${diff.summary.warnings}`);
|
|
559
|
+
lines.push(` Additions: ${diff.summary.additions}`);
|
|
560
|
+
lines.push(` Removals: ${diff.summary.removals}`);
|
|
561
|
+
|
|
562
|
+
// Source comparison if available
|
|
563
|
+
if (diff.sourceComparison) {
|
|
564
|
+
lines.push("");
|
|
565
|
+
lines.push(`${bold}📝 Source Analysis:${reset}`);
|
|
566
|
+
lines.push(
|
|
567
|
+
` Functions: ${diff.sourceComparison.from?.totalFunctions || 0} → ${diff.sourceComparison.to?.totalFunctions || 0}`,
|
|
568
|
+
);
|
|
569
|
+
lines.push(
|
|
570
|
+
` Avg Complexity: ${diff.sourceComparison.from?.avgComplexity || 0} → ${diff.sourceComparison.to?.avgComplexity || 0}`,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return lines.join("\n");
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Format diff result as JSON
|
|
579
|
+
*/
|
|
580
|
+
export function formatDiffAsJson(diff) {
|
|
581
|
+
return JSON.stringify(diff, null, 2);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export default {
|
|
585
|
+
compareVersions,
|
|
586
|
+
formatDiffAsText,
|
|
587
|
+
formatDiffAsJson,
|
|
588
|
+
ChangeType,
|
|
589
|
+
Severity,
|
|
590
|
+
};
|