@kosdev-code/kos-ui-cli 2.0.45 → 2.0.46

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.
@@ -1,748 +0,0 @@
1
- import { readFileSync, existsSync, writeFileSync } from 'fs';
2
- import { join } from 'path';
3
- import chalk from 'chalk';
4
- import { getProjectDetails } from '../../../utils/nx-context.mjs';
5
-
6
- /**
7
- * Compares two OpenAPI versions and generates a detailed compatibility report.
8
- *
9
- * Analyzes generated TypeScript type definitions (openapi.d.ts files) to detect
10
- * breaking changes, new endpoints, and API modifications. Supports multiple output
11
- * formats and can compare multiple app namespaces simultaneously.
12
- *
13
- * @param {Object} options - Comparison options
14
- * @param {string} options.project - Project name containing generated API types
15
- * @param {string} [options.app] - Single app namespace to compare (e.g., 'kos', 'dispense')
16
- * @param {string[]} [options.apps] - Array of app namespaces for multi-app comparison
17
- * @param {string} options.baseVersion - Base version identifier (e.g., 'v1.8.0')
18
- * @param {string} options.targetVersion - Target version identifier (e.g., 'v1.9.0')
19
- * @param {('console'|'json'|'markdown')} [options.format='console'] - Output format
20
- * @param {string} [options.outputFile] - Optional file path to write output
21
- * @returns {Promise<{compatible: boolean, results: Array}>} Comparison results with compatibility status
22
- * @throws {Error} When project not found or required type files don't exist
23
- *
24
- * @example
25
- * ```javascript
26
- * // Basic comparison
27
- * const result = await compareApiVersions({
28
- * project: 'device-models',
29
- * app: 'kos',
30
- * baseVersion: 'v1.8.0',
31
- * targetVersion: 'v1.9.0',
32
- * format: 'console'
33
- * });
34
- *
35
- * if (!result.compatible) {
36
- * console.error('Breaking changes detected!');
37
- * process.exit(1);
38
- * }
39
- * ```
40
- *
41
- * @example
42
- * ```javascript
43
- * // Multi-app comparison with markdown output
44
- * await compareApiVersions({
45
- * project: 'device-models',
46
- * apps: ['kos', 'dispense', 'freestyle'],
47
- * baseVersion: 'v1.8.0',
48
- * targetVersion: 'v1.9.0',
49
- * format: 'markdown',
50
- * outputFile: 'CHANGELOG-API.md'
51
- * });
52
- * ```
53
- */
54
- export async function compareApiVersions(options) {
55
- const {
56
- project,
57
- app,
58
- apps, // Array for multi-app comparison
59
- baseVersion,
60
- targetVersion,
61
- format = 'console',
62
- outputFile,
63
- } = options;
64
-
65
- // Get project configuration (following generate-api.mjs pattern exactly)
66
- let projectConfig, src, outputPath;
67
-
68
- try {
69
- projectConfig = await getProjectDetails(project);
70
- src = projectConfig.sourceRoot || projectConfig.root;
71
- outputPath = projectConfig.targets?.api?.options?.outputPath;
72
- } catch (error) {
73
- throw new Error(`Project "${project}" not found in workspace: ${error.message}`);
74
- }
75
-
76
- // Use custom output path if specified, otherwise default to src/utils
77
- const utilsDir = outputPath ? join(src, outputPath) : join(src, "utils");
78
- const servicesDir = join(utilsDir, "services");
79
-
80
- // Determine which apps to compare
81
- const appsToCompare = apps || [app];
82
- const results = [];
83
-
84
- // Compare each app
85
- for (const appName of appsToCompare) {
86
- const baseSpecPath = join(servicesDir, appName, baseVersion, 'openapi.d.ts');
87
- const targetSpecPath = join(servicesDir, appName, targetVersion, 'openapi.d.ts');
88
-
89
- // Validate files exist
90
- if (!existsSync(baseSpecPath)) {
91
- throw new Error(
92
- `Base version not found: ${baseSpecPath}\n` +
93
- `Generate it by running: kosui api:generate --project ${project}\n` +
94
- `(Point to ${baseVersion} device first)`
95
- );
96
- }
97
-
98
- if (!existsSync(targetSpecPath)) {
99
- throw new Error(
100
- `Target version not found: ${targetSpecPath}\n` +
101
- `Generate it by running: kosui api:generate --project ${project}\n` +
102
- `(Point to ${targetVersion} device first)`
103
- );
104
- }
105
-
106
- // Load and parse specs
107
- const baseContent = readFileSync(baseSpecPath, 'utf-8');
108
- const targetContent = readFileSync(targetSpecPath, 'utf-8');
109
-
110
- const basePaths = extractPaths(baseContent);
111
- const targetPaths = extractPaths(targetContent);
112
-
113
- // Perform comparison
114
- const comparison = compareSpecs(basePaths, targetPaths, {
115
- app: appName,
116
- baseVersion,
117
- targetVersion,
118
- });
119
-
120
- results.push(comparison);
121
- }
122
-
123
- // Format and output results
124
- const formatted = formatResults(results, format);
125
-
126
- if (outputFile) {
127
- writeFileSync(outputFile, formatted, 'utf-8');
128
- } else {
129
- console.log(formatted);
130
- }
131
-
132
- // Return exit code based on compatibility
133
- const hasBreakingChanges = results.some(r => !r.summary.compatible);
134
- process.exitCode = hasBreakingChanges ? 1 : 0;
135
-
136
- return {
137
- compatible: !hasBreakingChanges,
138
- results,
139
- };
140
- }
141
-
142
- /**
143
- * Extracts API paths and their HTTP methods from OpenAPI TypeScript type definitions.
144
- *
145
- * Parses the `export interface paths` section of a generated openapi.d.ts file
146
- * to build a map of endpoint paths to their available HTTP methods and parameters.
147
- *
148
- * @param {string} content - Content of openapi.d.ts file
149
- * @returns {Map<string, Map<string, Object>>} Map of paths to methods with parameter info
150
- *
151
- * @example
152
- * ```javascript
153
- * const content = readFileSync('openapi.d.ts', 'utf-8');
154
- * const paths = extractPaths(content);
155
- * // paths.get('/api/kos/device/status') → Map { 'get' => { params: {...} } }
156
- * ```
157
- */
158
- function extractPaths(content) {
159
- const paths = new Map();
160
- const pathsMatch = content.match(/export interface paths \{([\s\S]*?)\n\}/);
161
-
162
- if (!pathsMatch) {
163
- return paths;
164
- }
165
-
166
- const pathsContent = pathsMatch[1];
167
- const lines = pathsContent.split('\n');
168
- let currentPath = null;
169
- let braceCount = 0;
170
- let pathContent = '';
171
-
172
- for (const line of lines) {
173
- // Match path property (quoted string at start of line)
174
- const pathMatch = line.match(/^\s*"([^"]+)":\s*\{/);
175
-
176
- if (pathMatch && braceCount === 0) {
177
- currentPath = pathMatch[1];
178
- braceCount = 1;
179
- pathContent = line;
180
- } else if (currentPath) {
181
- pathContent += '\n' + line;
182
- braceCount += (line.match(/\{/g) || []).length;
183
- braceCount -= (line.match(/\}/g) || []).length;
184
-
185
- if (braceCount === 0) {
186
- const methods = extractMethods(pathContent);
187
- paths.set(currentPath, methods);
188
- currentPath = null;
189
- pathContent = '';
190
- }
191
- }
192
- }
193
-
194
- return paths;
195
- }
196
-
197
- /**
198
- * Extracts HTTP methods and their parameters from a path definition string.
199
- *
200
- * Parses the TypeScript definition for a single path to identify available
201
- * HTTP methods (GET, POST, PUT, DELETE, PATCH) and their query, path, and
202
- * body parameters.
203
- *
204
- * @param {string} pathDef - TypeScript definition string for a single path
205
- * @returns {Map<string, Object>} Map of method names to parameter definitions
206
- *
207
- * @example
208
- * ```javascript
209
- * const pathDef = `"/api/kos/device/status": {
210
- * get: { parameters: { query: { detailed?: boolean } } }
211
- * }`;
212
- * const methods = extractMethods(pathDef);
213
- * // methods.get('get') → { params: { query: [...], path: [...], body: '...' } }
214
- * ```
215
- */
216
- function extractMethods(pathDef) {
217
- const methods = new Map();
218
- const methodNames = ['get', 'post', 'put', 'delete', 'patch'];
219
-
220
- for (const methodName of methodNames) {
221
- // Match method property (unquoted identifier with any indentation)
222
- const methodRegex = new RegExp(`^\\s*${methodName}:\\s*\\{`, 'gm');
223
- const match = methodRegex.exec(pathDef);
224
-
225
- if (match) {
226
- const startIdx = match.index;
227
- let braceCount = 1;
228
- let endIdx = startIdx + match[0].length;
229
-
230
- for (let i = endIdx; i < pathDef.length; i++) {
231
- if (pathDef[i] === '{') braceCount++;
232
- if (pathDef[i] === '}') braceCount--;
233
- if (braceCount === 0) {
234
- endIdx = i + 1;
235
- break;
236
- }
237
- }
238
-
239
- const methodDef = pathDef.substring(startIdx, endIdx);
240
-
241
- const params = {
242
- query: extractParameters(methodDef, 'query'),
243
- path: extractParameters(methodDef, 'path'),
244
- body: extractRequestBody(methodDef)
245
- };
246
-
247
- methods.set(methodName, { params });
248
- }
249
- }
250
-
251
- return methods;
252
- }
253
-
254
- /**
255
- * Extracts parameters of a specific type from an HTTP method definition.
256
- *
257
- * Parses query or path parameters from the TypeScript definition, identifying
258
- * parameter names and whether they are required or optional.
259
- *
260
- * @param {string} methodDef - TypeScript definition for an HTTP method
261
- * @param {('query'|'path')} paramType - Type of parameters to extract
262
- * @returns {Array<{name: string, required: boolean}>} Array of parameter definitions
263
- *
264
- * @example
265
- * ```javascript
266
- * const methodDef = `get: { parameters: { query: { id: string, optional?: string } } }`;
267
- * const params = extractParameters(methodDef, 'query');
268
- * // Returns: [{ name: 'id', required: true }, { name: 'optional', required: false }]
269
- * ```
270
- */
271
- function extractParameters(methodDef, paramType) {
272
- const params = [];
273
- const paramMatch = methodDef.match(new RegExp(`${paramType}[?]?:\\s*\\{([\\s\\S]*?)\\n\\s*\\}`, 'm'));
274
-
275
- if (paramMatch) {
276
- const paramContent = paramMatch[1];
277
- const paramRegex = /(\w+)(\?)?:/g;
278
- let match;
279
-
280
- while ((match = paramRegex.exec(paramContent)) !== null) {
281
- if (match[1] !== 'never' && match[1] !== 'header' && match[1] !== 'cookie') {
282
- params.push({
283
- name: match[1],
284
- required: !match[2]
285
- });
286
- }
287
- }
288
- }
289
-
290
- return params;
291
- }
292
-
293
- /**
294
- * Checks if an HTTP method definition includes a request body.
295
- *
296
- * Determines whether the endpoint expects a request body by checking for
297
- * the presence of requestBody schema in the TypeScript definition.
298
- *
299
- * @param {string} methodDef - TypeScript definition for an HTTP method
300
- * @returns {('present'|'none')} Whether a request body is expected
301
- *
302
- * @example
303
- * ```javascript
304
- * const methodDef = `post: { requestBody: { schema: {...} } }`;
305
- * extractRequestBody(methodDef); // Returns: 'present'
306
- * ```
307
- */
308
- function extractRequestBody(methodDef) {
309
- const bodyMatch = methodDef.match(/requestBody[:\?]\s*\{[\s\S]*?schema:/);
310
- return bodyMatch ? 'present' : 'none';
311
- }
312
-
313
- /**
314
- * Compares parameters between two API versions to detect changes.
315
- *
316
- * Identifies added, removed, and modified parameters, classifying changes
317
- * as either breaking (removed params, new required params) or safe
318
- * (new optional params, optional becoming required).
319
- *
320
- * @param {Array<{name: string, required: boolean}>} baseParams - Parameters from base version
321
- * @param {Array<{name: string, required: boolean}>} targetParams - Parameters from target version
322
- * @returns {Array<{type: string, param: string, severity: string}>} Array of parameter changes
323
- *
324
- * @example
325
- * ```javascript
326
- * const base = [{ name: 'id', required: true }];
327
- * const target = [{ name: 'id', required: false }, { name: 'filter', required: true }];
328
- * const changes = compareParameters(base, target);
329
- * // Returns changes including:
330
- * // - { type: 'changed', param: 'id', change: 'now optional', severity: 'safe' }
331
- * // - { type: 'added', param: 'filter', required: true, severity: 'breaking' }
332
- * ```
333
- */
334
- function compareParameters(baseParams, targetParams) {
335
- const changes = [];
336
- const baseNames = new Set(baseParams.map(p => p.name));
337
- const targetNames = new Set(targetParams.map(p => p.name));
338
-
339
- // Removed parameters (breaking)
340
- for (const param of baseParams) {
341
- if (!targetNames.has(param.name)) {
342
- changes.push({ type: 'removed', param: param.name, severity: 'breaking' });
343
- }
344
- }
345
-
346
- // Added parameters
347
- for (const param of targetParams) {
348
- if (!baseNames.has(param.name)) {
349
- const severity = param.required ? 'breaking' : 'safe';
350
- changes.push({ type: 'added', param: param.name, required: param.required, severity });
351
- }
352
- }
353
-
354
- // Changed required status
355
- for (const baseParam of baseParams) {
356
- const targetParam = targetParams.find(p => p.name === baseParam.name);
357
- if (targetParam && baseParam.required !== targetParam.required) {
358
- const severity = targetParam.required ? 'breaking' : 'safe';
359
- changes.push({
360
- type: 'changed',
361
- param: baseParam.name,
362
- change: targetParam.required ? 'now required' : 'now optional',
363
- severity
364
- });
365
- }
366
- }
367
-
368
- return changes;
369
- }
370
-
371
- /**
372
- * Performs comprehensive comparison of two OpenAPI specifications.
373
- *
374
- * Analyzes paths, methods, and parameters to generate a detailed compatibility
375
- * report including breaking changes, new endpoints, and modifications. The
376
- * comparison identifies API evolution and determines backward compatibility.
377
- *
378
- * @param {Map<string, Map<string, Object>>} basePaths - Parsed paths from base version
379
- * @param {Map<string, Map<string, Object>>} targetPaths - Parsed paths from target version
380
- * @param {Object} metadata - Version metadata (app, baseVersion, targetVersion)
381
- * @returns {Object} Comprehensive comparison results with summary and detailed changes
382
- *
383
- * @example
384
- * ```javascript
385
- * const basePaths = extractPaths(baseContent);
386
- * const targetPaths = extractPaths(targetContent);
387
- * const results = compareSpecs(basePaths, targetPaths, {
388
- * app: 'kos',
389
- * baseVersion: 'v1.8.0',
390
- * targetVersion: 'v1.9.0'
391
- * });
392
- *
393
- * console.log(`Compatible: ${results.summary.compatible}`);
394
- * console.log(`Breaking changes: ${results.summary.breakingChanges.length}`);
395
- * ```
396
- */
397
- function compareSpecs(basePaths, targetPaths, metadata) {
398
- const results = {
399
- metadata,
400
- summary: {
401
- totalPathsBase: basePaths.size,
402
- totalPathsTarget: targetPaths.size,
403
- addedPaths: [],
404
- removedPaths: [],
405
- modifiedPaths: [],
406
- compatible: true,
407
- breakingChanges: []
408
- }
409
- };
410
-
411
- // Find added paths
412
- for (const [path, methods] of targetPaths) {
413
- if (!basePaths.has(path)) {
414
- results.summary.addedPaths.push({
415
- path,
416
- methods: Array.from(methods.keys()).map(m => m.toUpperCase())
417
- });
418
- }
419
- }
420
-
421
- // Find removed paths (breaking)
422
- for (const [path, methods] of basePaths) {
423
- if (!targetPaths.has(path)) {
424
- results.summary.removedPaths.push({
425
- path,
426
- methods: Array.from(methods.keys()).map(m => m.toUpperCase())
427
- });
428
- results.summary.compatible = false;
429
- results.summary.breakingChanges.push({
430
- path,
431
- change: `${Array.from(methods.keys()).map(m => m.toUpperCase()).join(', ')} removed`
432
- });
433
- }
434
- }
435
-
436
- // Find modified paths
437
- for (const [path, baseMethods] of basePaths) {
438
- if (!targetPaths.has(path)) continue;
439
-
440
- const targetMethods = targetPaths.get(path);
441
- const changes = [];
442
-
443
- // Check for removed methods (breaking)
444
- for (const [method, baseMethodDef] of baseMethods) {
445
- if (!targetMethods.has(method)) {
446
- changes.push(`${method.toUpperCase()} method removed`);
447
- results.summary.compatible = false;
448
- results.summary.breakingChanges.push({
449
- path,
450
- change: `${method.toUpperCase()} method removed`
451
- });
452
- }
453
- }
454
-
455
- // Check for added methods (safe)
456
- for (const [method] of targetMethods) {
457
- if (!baseMethods.has(method)) {
458
- changes.push(`Added ${method.toUpperCase()} method`);
459
- }
460
- }
461
-
462
- // Check for parameter changes
463
- for (const [method, baseMethodDef] of baseMethods) {
464
- const targetMethodDef = targetMethods.get(method);
465
- if (!targetMethodDef) continue;
466
-
467
- // Query parameters
468
- const queryChanges = compareParameters(baseMethodDef.params.query, targetMethodDef.params.query);
469
- for (const change of queryChanges) {
470
- const changeDesc = `${method.toUpperCase()} query parameter "${change.param}" ${change.type}${change.change ? ` (${change.change})` : ''}`;
471
-
472
- if (change.severity === 'breaking') {
473
- results.summary.compatible = false;
474
- results.summary.breakingChanges.push({ path, change: changeDesc });
475
- } else {
476
- changes.push(changeDesc);
477
- }
478
- }
479
-
480
- // Path parameters
481
- const pathChanges = compareParameters(baseMethodDef.params.path, targetMethodDef.params.path);
482
- for (const change of pathChanges) {
483
- const changeDesc = `${method.toUpperCase()} path parameter "${change.param}" ${change.type}${change.change ? ` (${change.change})` : ''}`;
484
-
485
- if (change.severity === 'breaking') {
486
- results.summary.compatible = false;
487
- results.summary.breakingChanges.push({ path, change: changeDesc });
488
- } else {
489
- changes.push(changeDesc);
490
- }
491
- }
492
-
493
- // Request body changes
494
- if (baseMethodDef.params.body !== targetMethodDef.params.body) {
495
- const changeDesc = targetMethodDef.params.body === 'present' && baseMethodDef.params.body === 'none'
496
- ? `${method.toUpperCase()} request body now required`
497
- : `${method.toUpperCase()} request body changed`;
498
-
499
- const severity = targetMethodDef.params.body === 'present' && baseMethodDef.params.body === 'none' ? 'breaking' : 'safe';
500
-
501
- if (severity === 'breaking') {
502
- results.summary.compatible = false;
503
- results.summary.breakingChanges.push({ path, change: changeDesc });
504
- } else {
505
- changes.push(changeDesc);
506
- }
507
- }
508
- }
509
-
510
- if (changes.length > 0) {
511
- results.summary.modifiedPaths.push({ path, changes });
512
- }
513
- }
514
-
515
- return results;
516
- }
517
-
518
- /**
519
- * Formats comparison results according to the specified output format.
520
- *
521
- * Delegates to format-specific functions to generate console, JSON, or
522
- * markdown output suitable for different use cases (terminal display,
523
- * automation, documentation).
524
- *
525
- * @param {Array<Object>} results - Array of comparison results (one per app)
526
- * @param {('console'|'json'|'markdown')} format - Desired output format
527
- * @returns {string} Formatted comparison output
528
- *
529
- * @example
530
- * ```javascript
531
- * const results = [{ metadata: {...}, summary: {...} }];
532
- * const output = formatResults(results, 'markdown');
533
- * writeFileSync('CHANGELOG.md', output);
534
- * ```
535
- */
536
- function formatResults(results, format) {
537
- switch (format) {
538
- case 'json':
539
- return formatJson(results);
540
- case 'markdown':
541
- return formatMarkdown(results);
542
- case 'console':
543
- default:
544
- return formatConsole(results);
545
- }
546
- }
547
-
548
- /**
549
- * Formats comparison results as JSON for machine-readable output.
550
- *
551
- * Generates a structured JSON representation suitable for automation,
552
- * CI/CD pipelines, and programmatic analysis.
553
- *
554
- * @param {Array<Object>} results - Array of comparison results
555
- * @returns {string} JSON-formatted comparison data
556
- *
557
- * @example
558
- * ```javascript
559
- * const json = formatJson(results);
560
- * const parsed = JSON.parse(json);
561
- * console.log(`Breaking changes: ${parsed[0].summary.breakingChanges.length}`);
562
- * ```
563
- */
564
- function formatJson(results) {
565
- return JSON.stringify(results, null, 2);
566
- }
567
-
568
- /**
569
- * Formats comparison results as Markdown for documentation and release notes.
570
- *
571
- * Generates well-structured markdown with sections for summary, breaking changes,
572
- * new endpoints, removed endpoints, and modifications. Suitable for changelogs,
573
- * release notes, and technical documentation.
574
- *
575
- * @param {Array<Object>} results - Array of comparison results
576
- * @returns {string} Markdown-formatted comparison report
577
- *
578
- * @example
579
- * ```javascript
580
- * const markdown = formatMarkdown(results);
581
- * writeFileSync('CHANGELOG-API.md', markdown);
582
- * ```
583
- */
584
- function formatMarkdown(results) {
585
- let output = [];
586
-
587
- for (const result of results) {
588
- const { metadata, summary } = result;
589
-
590
- output.push(`# API Changes: ${metadata.app} (${metadata.baseVersion} → ${metadata.targetVersion})\n`);
591
-
592
- // Summary
593
- output.push(`## Summary\n`);
594
- output.push(`- Total endpoints: ${summary.totalPathsBase} → ${summary.totalPathsTarget}`);
595
- output.push(`- Added: ${summary.addedPaths.length}`);
596
- output.push(`- Removed: ${summary.removedPaths.length}`);
597
- output.push(`- Modified: ${summary.modifiedPaths.length}`);
598
- output.push(`- Breaking changes: ${summary.breakingChanges.length}`);
599
- output.push(`- **Status**: ${summary.compatible ? '✅ Backward compatible' : '❌ Not backward compatible'}\n`);
600
-
601
- // Breaking changes
602
- if (summary.breakingChanges.length > 0) {
603
- output.push(`## Breaking Changes\n`);
604
- for (const change of summary.breakingChanges) {
605
- output.push(`- \`${change.path}\` - ${change.change}`);
606
- }
607
- output.push('');
608
- }
609
-
610
- // New endpoints
611
- if (summary.addedPaths.length > 0) {
612
- output.push(`## New Endpoints\n`);
613
- for (const { path, methods } of summary.addedPaths) {
614
- output.push(`- \`${path}\` - ${methods.join(', ')}`);
615
- }
616
- output.push('');
617
- }
618
-
619
- // Removed endpoints
620
- if (summary.removedPaths.length > 0) {
621
- output.push(`## Removed Endpoints\n`);
622
- for (const { path, methods } of summary.removedPaths) {
623
- output.push(`- \`${path}\` - ${methods.join(', ')}`);
624
- }
625
- output.push('');
626
- }
627
-
628
- // Modified endpoints
629
- if (summary.modifiedPaths.length > 0) {
630
- output.push(`## Modified Endpoints (Non-Breaking)\n`);
631
- for (const { path, changes } of summary.modifiedPaths) {
632
- output.push(`- \`${path}\` - ${changes.join(', ')}`);
633
- }
634
- output.push('');
635
- }
636
- }
637
-
638
- return output.join('\n');
639
- }
640
-
641
- /**
642
- * Formats comparison results with colored terminal output for human readability.
643
- *
644
- * Generates visually appealing console output with color-coded sections,
645
- * compatibility status, and actionable recommendations. Uses chalk for
646
- * terminal color support.
647
- *
648
- * @param {Array<Object>} results - Array of comparison results
649
- * @returns {string} Colored terminal output with ASCII decorations
650
- *
651
- * @example
652
- * ```javascript
653
- * const output = formatConsole(results);
654
- * console.log(output); // Displays colored comparison in terminal
655
- * ```
656
- */
657
- function formatConsole(results) {
658
- let output = [];
659
-
660
- for (const result of results) {
661
- const { metadata, summary } = result;
662
-
663
- // Header
664
- output.push(chalk.gray('═'.repeat(63)));
665
- output.push(chalk.bold.cyan('API Compatibility Analysis\n'));
666
- output.push(chalk.white(`App: ${metadata.app}`));
667
- output.push(chalk.white(`Base Version: ${metadata.baseVersion}`));
668
- output.push(chalk.white(`Target Version: ${metadata.targetVersion}`));
669
- output.push(chalk.gray('═'.repeat(63)) + '\n');
670
-
671
- // Compatibility status
672
- if (summary.compatible) {
673
- output.push(chalk.green.bold('BACKWARD COMPATIBLE\n'));
674
- } else {
675
- output.push(chalk.red.bold('BREAKING CHANGES DETECTED\n'));
676
- }
677
-
678
- output.push(chalk.gray('═'.repeat(63)) + '\n');
679
-
680
- // Summary
681
- output.push(chalk.bold('Summary\n'));
682
- output.push(chalk.white(`Total Paths in Base: ${summary.totalPathsBase}`));
683
- output.push(chalk.white(`Total Paths in Target: ${summary.totalPathsTarget}`));
684
- output.push(chalk.white(`Added Paths: ${summary.addedPaths.length}`));
685
- output.push(chalk.white(`Removed Paths: ${summary.removedPaths.length}`));
686
- output.push(chalk.white(`Modified Paths: ${summary.modifiedPaths.length}`));
687
- output.push(chalk.white(`Breaking Changes: ${summary.breakingChanges.length}`));
688
- output.push('');
689
-
690
- // Breaking changes
691
- if (summary.breakingChanges.length > 0) {
692
- output.push(chalk.gray('═'.repeat(63)) + '\n');
693
- output.push(chalk.red.bold('BREAKING CHANGES\n'));
694
- for (const change of summary.breakingChanges) {
695
- output.push(chalk.yellow(`${change.path}`) + chalk.white(` - ${change.change}`));
696
- }
697
- output.push('');
698
- }
699
-
700
- // New endpoints
701
- if (summary.addedPaths.length > 0) {
702
- output.push(chalk.gray('═'.repeat(63)) + '\n');
703
- output.push(chalk.green.bold('NEW ENDPOINTS\n'));
704
- for (const { path, methods } of summary.addedPaths) {
705
- output.push(chalk.cyan(path) + chalk.white(` - ${methods.join(', ')}`));
706
- }
707
- output.push('');
708
- }
709
-
710
- // Removed endpoints
711
- if (summary.removedPaths.length > 0) {
712
- output.push(chalk.gray('═'.repeat(63)) + '\n');
713
- output.push(chalk.red.bold('REMOVED ENDPOINTS\n'));
714
- for (const { path, methods } of summary.removedPaths) {
715
- output.push(chalk.red(path) + chalk.white(` - ${methods.join(', ')}`));
716
- }
717
- output.push('');
718
- }
719
-
720
- // Modified endpoints
721
- if (summary.modifiedPaths.length > 0) {
722
- output.push(chalk.gray('═'.repeat(63)) + '\n');
723
- output.push(chalk.blue.bold('MODIFIED ENDPOINTS (Non-Breaking)\n'));
724
- for (const { path, changes } of summary.modifiedPaths) {
725
- output.push(chalk.cyan(path) + chalk.white(` - ${changes.join(', ')}`));
726
- }
727
- output.push('');
728
- }
729
-
730
- output.push(chalk.gray('═'.repeat(63)) + '\n');
731
-
732
- // Recommendations
733
- output.push(chalk.bold('Recommendations\n'));
734
- if (!summary.compatible) {
735
- output.push(chalk.yellow('Migration Required - Breaking changes detected'));
736
- output.push(chalk.white('Review all breaking changes before upgrading'));
737
- } else if (summary.addedPaths.length > 0) {
738
- output.push(chalk.green('Safe to upgrade - No breaking changes'));
739
- output.push(chalk.white('New endpoints available to use'));
740
- } else {
741
- output.push(chalk.green('Versions are identical or fully compatible'));
742
- }
743
-
744
- output.push('\n' + chalk.gray('═'.repeat(63)));
745
- }
746
-
747
- return output.join('\n');
748
- }