@sun-asterisk/sunlint 1.3.31 → 1.3.33

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/README.md CHANGED
@@ -19,6 +19,7 @@ Sun Lint is a universal coding standards checker providing comprehensive code qu
19
19
  - ✅ **Advanced File Targeting**: Include/exclude patterns, language filtering
20
20
  - ✅ **Quality Scoring System**: Automated quality score (0-100) with grade (A+ to F)
21
21
  - ✅ **Summary Reports**: JSON format for CI/CD dashboards and management reports
22
+ - ✅ **Architecture Detection**: Detect architecture patterns (Layered, Modular, MVVM, VIPER) with health scoring
22
23
 
23
24
  ### **🏗️ Architecture**
24
25
 
@@ -128,6 +129,52 @@ sunlint --all --input=src --output-summary=quality.json
128
129
  - Quality gate setup
129
130
  - Trending analysis
130
131
 
132
+ ## 🏛️ **Architecture Detection** 🆕
133
+
134
+ Detect and analyze architecture patterns in your codebase with health scoring and violation detection.
135
+
136
+ ### **Features**
137
+ - **Pattern Detection**: Layered, Modular, MVVM, VIPER, Clean Architecture
138
+ - **Health Score (0-100)**: Architecture compliance scoring
139
+ - **Violation Detection**: Identify architecture anti-patterns
140
+ - **Markdown Reports**: Detailed analysis reports for documentation
141
+
142
+ ### **Quick Usage**
143
+ ```bash
144
+ # Architecture analysis only
145
+ sunlint --architecture --input=src
146
+
147
+ # Combined: Code quality + Architecture
148
+ sunlint --all --architecture --input=src
149
+
150
+ # Generate markdown report
151
+ sunlint --architecture --arch-report --input=src
152
+
153
+ # Target specific patterns
154
+ sunlint --architecture --arch-patterns=mvvm,layered --input=src
155
+ ```
156
+
157
+ ### **Example Output**
158
+ ```
159
+ 🏛️ Architecture Analysis:
160
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
161
+ • Pattern: LAYERED (69% confidence)
162
+ • Health Score: 85/100
163
+ • Violations: 3
164
+
165
+ Top Architecture Violations:
166
+ ⚠ Interface/Contract Definitions - score 20% (threshold: 50%)
167
+ ⚠ Middleware/Interceptor Layer - score 0% (threshold: 50%)
168
+ ... and 1 more
169
+ ```
170
+
171
+ ### **Options**
172
+ | Option | Description |
173
+ |--------|-------------|
174
+ | `--architecture` | Enable architecture pattern detection |
175
+ | `--arch-patterns <patterns>` | Target specific patterns (comma-separated: layered, modular, mvvm, viper) |
176
+ | `--arch-report` | Generate separate markdown report |
177
+
131
178
  ## 📦 **Installation**
132
179
 
133
180
  ### **Global Installation (Recommended)**
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Architecture Integration for SunLint
3
+ * Wraps architecture-detection module for seamless integration
4
+ * Following Rule C005: Single responsibility - handle architecture analysis integration
5
+ */
6
+
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const chalk = require('chalk');
10
+
11
+ class ArchitectureIntegration {
12
+ constructor(options = {}) {
13
+ this.options = options;
14
+ this.archModule = null;
15
+ }
16
+
17
+ /**
18
+ * Load architecture detection module
19
+ * Tries bundled version first, then falls back to local development path
20
+ */
21
+ async loadArchitectureModule() {
22
+ if (this.archModule) {
23
+ return this.archModule;
24
+ }
25
+
26
+ // Try bundled version first (engines/arch-detect)
27
+ const bundledPath = path.join(__dirname, '..', 'engines', 'arch-detect', 'index.js');
28
+
29
+ if (fs.existsSync(bundledPath)) {
30
+ try {
31
+ this.archModule = require(bundledPath);
32
+ if (this.options.verbose) {
33
+ console.log(chalk.gray('📦 Loaded bundled architecture-detection'));
34
+ }
35
+ return this.archModule;
36
+ } catch (error) {
37
+ if (this.options.verbose) {
38
+ console.log(chalk.yellow(`⚠️ Failed to load bundled module: ${error.message}`));
39
+ }
40
+ }
41
+ }
42
+
43
+ // Fallback: Try local development path
44
+ const devPaths = [
45
+ path.join(__dirname, '..', '..', '..', '..', 'architecture-detection', 'dist', 'index.js'),
46
+ path.join(__dirname, '..', '..', '..', 'architecture-detection', 'dist', 'index.js'),
47
+ ];
48
+
49
+ for (const devPath of devPaths) {
50
+ if (fs.existsSync(devPath)) {
51
+ try {
52
+ this.archModule = require(devPath);
53
+ if (this.options.verbose) {
54
+ console.log(chalk.gray(`📦 Loaded architecture-detection from: ${devPath}`));
55
+ }
56
+ return this.archModule;
57
+ } catch (error) {
58
+ if (this.options.verbose) {
59
+ console.log(chalk.yellow(`⚠️ Failed to load from ${devPath}: ${error.message}`));
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ throw new Error(
66
+ 'Architecture detection module not found. Run "npm run build" to bundle it, ' +
67
+ 'or ensure architecture-detection is built in the parent directory.'
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Parse architecture patterns from CLI option
73
+ */
74
+ parsePatterns() {
75
+ if (!this.options.archPatterns) {
76
+ return undefined; // Use default patterns
77
+ }
78
+
79
+ const patternMap = {
80
+ 'layered': 'LAYERED',
81
+ 'modular': 'MODULAR',
82
+ 'mvvm': 'MVVM',
83
+ 'viper': 'VIPER',
84
+ 'presentation': 'PRESENTATION',
85
+ 'clean': 'CLEAN_ARCHITECTURE',
86
+ 'tdd': 'TDD_CLEAN_ARCHITECTURE',
87
+ };
88
+
89
+ const patterns = this.options.archPatterns
90
+ .split(',')
91
+ .map(p => p.trim().toLowerCase())
92
+ .map(p => patternMap[p] || p.toUpperCase())
93
+ .filter(Boolean);
94
+
95
+ return patterns.length > 0 ? patterns : undefined;
96
+ }
97
+
98
+ /**
99
+ * Run architecture analysis on project
100
+ * @param {string} projectPath - Path to analyze
101
+ * @returns {Object} Architecture analysis results
102
+ */
103
+ async analyze(projectPath) {
104
+ const archModule = await this.loadArchitectureModule();
105
+ const { ArchitectureAnalyzer } = archModule;
106
+
107
+ if (!ArchitectureAnalyzer) {
108
+ throw new Error('ArchitectureAnalyzer not found in architecture-detection module');
109
+ }
110
+
111
+ const analyzer = new ArchitectureAnalyzer({
112
+ patterns: this.parsePatterns(),
113
+ respectGitignore: true,
114
+ verbose: this.options.verbose,
115
+ });
116
+
117
+ if (this.options.verbose) {
118
+ console.log(chalk.blue(`🏛️ Analyzing architecture: ${projectPath}`));
119
+ }
120
+
121
+ const result = await analyzer.analyze(projectPath);
122
+
123
+ // Convert to SunLint-compatible format
124
+ return this.convertToSunLintFormat(result, analyzer, projectPath);
125
+ }
126
+
127
+ /**
128
+ * Convert architecture results to SunLint format
129
+ */
130
+ convertToSunLintFormat(result, analyzer, projectPath) {
131
+ const violations = [];
132
+
133
+ // Convert architecture violations to SunLint violation format
134
+ // Check violationAssessment.violations from the new format
135
+ const violationAssessment = result.violationAssessment;
136
+ if (violationAssessment && violationAssessment.violations && violationAssessment.violations.length > 0) {
137
+ for (const violation of violationAssessment.violations) {
138
+ violations.push({
139
+ ruleId: `ARCH-${violation.type || 'VIOLATION'}`,
140
+ severity: this.mapSeverity(violation.impact),
141
+ message: violation.description || violation.ruleName,
142
+ file: violation.affectedFiles?.[0] || projectPath,
143
+ line: 1,
144
+ column: 1,
145
+ category: 'architecture',
146
+ source: 'architecture-detection',
147
+ details: {
148
+ ruleName: violation.ruleName,
149
+ impactReason: violation.impactReason,
150
+ suggestedFix: violation.suggestedFix,
151
+ affectedFiles: violation.affectedFiles,
152
+ },
153
+ });
154
+ }
155
+ }
156
+
157
+ // Generate markdown report if requested
158
+ let markdownReport = null;
159
+ if (this.options.archReport) {
160
+ try {
161
+ markdownReport = analyzer.formatAsMarkdown(result);
162
+ } catch (error) {
163
+ if (this.options.verbose) {
164
+ console.log(chalk.yellow(`⚠️ Could not generate markdown report: ${error.message}`));
165
+ }
166
+ }
167
+ }
168
+
169
+ // Map from hybridAnalysis structure
170
+ const hybridAnalysis = result.hybridAnalysis || {};
171
+ const healthScore = violationAssessment?.healthScore || 100;
172
+
173
+ return {
174
+ summary: {
175
+ primaryPattern: hybridAnalysis.primaryPattern || result.primaryPattern || 'UNKNOWN',
176
+ primaryConfidence: hybridAnalysis.confidence || 0,
177
+ secondaryPatterns: (hybridAnalysis.secondaryPatterns || []).map(p => ({
178
+ pattern: p,
179
+ confidence: 0.5, // Default confidence for secondary patterns
180
+ })),
181
+ healthScore: healthScore,
182
+ violationCount: violations.length,
183
+ analysisTime: result.metadata?.analysisTimeMs || 0,
184
+ isHybrid: hybridAnalysis.isHybrid || false,
185
+ combination: hybridAnalysis.combination || null,
186
+ },
187
+ violations,
188
+ markdownReport,
189
+ raw: result,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Map architecture severity to SunLint severity
195
+ */
196
+ mapSeverity(severity) {
197
+ const severityMap = {
198
+ 'critical': 'error',
199
+ 'high': 'error',
200
+ 'medium': 'warning',
201
+ 'low': 'info',
202
+ };
203
+ return severityMap[severity?.toLowerCase()] || 'warning';
204
+ }
205
+
206
+ /**
207
+ * Save markdown report to file
208
+ */
209
+ async saveReport(markdownContent, projectPath) {
210
+ const projectName = path.basename(projectPath);
211
+ const date = new Date().toISOString().split('T')[0].replace(/-/g, '_');
212
+ const fileName = `sun_arch_report_${projectName}_${date}.md`;
213
+ const outputPath = path.join(process.cwd(), fileName);
214
+
215
+ fs.writeFileSync(outputPath, markdownContent, 'utf8');
216
+ return outputPath;
217
+ }
218
+ }
219
+
220
+ module.exports = { ArchitectureIntegration };
@@ -7,12 +7,14 @@
7
7
 
8
8
  const chalk = require('chalk');
9
9
  const fs = require('fs');
10
+ const path = require('path');
10
11
  const ConfigManager = require('./config-manager');
11
12
  const RuleSelectionService = require('./rule-selection-service');
12
13
  const AnalysisOrchestrator = require('./analysis-orchestrator');
13
14
  const OutputService = require('./output-service');
14
15
  const GitUtils = require('./git-utils');
15
16
  const FileTargetingService = require('./file-targeting-service');
17
+ const { ArchitectureIntegration } = require('./architecture-integration');
16
18
 
17
19
  // Legacy orchestrator for fallback
18
20
  // const LegacyOrchestrator = require('./legacy-analysis-orchestrator'); // Removed
@@ -79,11 +81,25 @@ class CliActionHandler {
79
81
 
80
82
  // Run analysis with appropriate orchestrator
81
83
  const startTime = Date.now();
82
- const results = await this.runModernAnalysis(rulesToRun, targetingResult.files, config);
84
+ let results = null;
85
+
86
+ // Run code quality analysis (unless --architecture is used alone)
87
+ if (rulesToRun.length > 0 && !this.isArchitectureOnly()) {
88
+ results = await this.runModernAnalysis(rulesToRun, targetingResult.files, config);
89
+ } else {
90
+ results = { results: [], summary: { total: 0, errors: 0, warnings: 0 } };
91
+ }
92
+
93
+ // Run architecture analysis if requested
94
+ if (this.options.architecture) {
95
+ const architectureResults = await this.runArchitectureAnalysis();
96
+ results.architecture = architectureResults;
97
+ }
98
+
83
99
  const duration = Date.now() - startTime;
84
100
 
85
101
  // Output results
86
- await this.outputService.outputResults(results, this.options, {
102
+ await this.outputService.outputResults(results, this.options, {
87
103
  duration,
88
104
  rulesRun: rulesToRun.length,
89
105
  rulesChecked: rulesToRun.length
@@ -121,9 +137,10 @@ class CliActionHandler {
121
137
  enabledEngines: this.determineEnabledEngines(config),
122
138
  aiConfig: config.ai || {},
123
139
  eslintConfig: config.eslint || {},
124
- heuristicConfig: {
140
+ heuristicConfig: {
125
141
  ...config.heuristic || {},
126
142
  targetFiles: this.options.targetFiles, // Pass filtered files for semantic optimization
143
+ projectPath: this.getProjectPath(), // Pass target project path for semantic engine
127
144
  maxSemanticFiles: this.options.maxSemanticFiles ? parseInt(this.options.maxSemanticFiles) : 1000,
128
145
  verbose: this.options.verbose // Pass verbose for debugging
129
146
  }
@@ -422,13 +439,39 @@ class CliActionHandler {
422
439
  */
423
440
  async applyFileTargeting(config) {
424
441
  // Handle both string and array input patterns
425
- const inputPaths = Array.isArray(this.options.input)
426
- ? this.options.input
442
+ const inputPaths = Array.isArray(this.options.input)
443
+ ? this.options.input
427
444
  : [this.options.input];
428
-
445
+
429
446
  return await this.fileTargetingService.getTargetFiles(inputPaths, config, this.options);
430
447
  }
431
448
 
449
+ /**
450
+ * Get project path from input for semantic engine
451
+ * Returns the directory path of the target for proper file resolution
452
+ */
453
+ getProjectPath() {
454
+ const input = Array.isArray(this.options.input)
455
+ ? this.options.input[0]
456
+ : this.options.input;
457
+
458
+ if (!input) {
459
+ return process.cwd();
460
+ }
461
+
462
+ const absolutePath = path.isAbsolute(input) ? input : path.resolve(process.cwd(), input);
463
+
464
+ // If input is a file, return its directory; if directory, return as-is
465
+ try {
466
+ if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile()) {
467
+ return path.dirname(absolutePath);
468
+ }
469
+ return absolutePath;
470
+ } catch {
471
+ return absolutePath;
472
+ }
473
+ }
474
+
432
475
  /**
433
476
  * Display analysis information
434
477
  * Following Rule C006: Verb-noun naming
@@ -466,18 +509,64 @@ class CliActionHandler {
466
509
  */
467
510
  handleExit(results) {
468
511
  if (this.options.noExit) return;
469
-
512
+
470
513
  // Check if any violations were found
471
- const hasViolations = results.results?.some(result =>
514
+ const hasViolations = results.results?.some(result =>
472
515
  result.violations && result.violations.length > 0
473
516
  );
474
-
517
+
475
518
  if (hasViolations && this.options.failOnViolations !== false) {
476
519
  process.exit(1);
477
520
  } else {
478
521
  process.exit(0);
479
522
  }
480
523
  }
524
+
525
+ /**
526
+ * Check if only architecture analysis was requested (no code quality rules)
527
+ * Following Rule C006: Verb-noun naming
528
+ */
529
+ isArchitectureOnly() {
530
+ return this.options.architecture &&
531
+ !this.options.all &&
532
+ !this.options.rule &&
533
+ !this.options.rules &&
534
+ !this.options.quality &&
535
+ !this.options.security &&
536
+ !this.options.category;
537
+ }
538
+
539
+ /**
540
+ * Run architecture analysis using architecture-detection module
541
+ * Following Rule C006: Verb-noun naming
542
+ */
543
+ async runArchitectureAnalysis() {
544
+ if (!this.options.quiet) {
545
+ console.log(chalk.blue('🏛️ Running architecture analysis...'));
546
+ }
547
+
548
+ try {
549
+ const integration = new ArchitectureIntegration(this.options);
550
+ const projectPath = this.getProjectPath();
551
+ const results = await integration.analyze(projectPath);
552
+
553
+ // Save markdown report if requested
554
+ if (this.options.archReport && results.markdownReport) {
555
+ const reportPath = await integration.saveReport(results.markdownReport, projectPath);
556
+ if (!this.options.quiet) {
557
+ console.log(chalk.green(`📄 Architecture report saved: ${reportPath}`));
558
+ }
559
+ }
560
+
561
+ return results;
562
+ } catch (error) {
563
+ console.error(chalk.yellow(`⚠️ Architecture analysis failed: ${error.message}`));
564
+ if (this.options.debug) {
565
+ console.error(error.stack);
566
+ }
567
+ return null;
568
+ }
569
+ }
481
570
  }
482
571
 
483
572
  module.exports = CliActionHandler;
@@ -23,6 +23,12 @@ function createCliProgram() {
23
23
  .option('--quality', 'Run all code quality rules')
24
24
  .option('--security', 'Run all secure coding rules');
25
25
 
26
+ // Architecture Analysis options
27
+ program
28
+ .option('--architecture', 'Enable architecture pattern detection (layered, modular, mvvm, viper)')
29
+ .option('--arch-patterns <patterns>', 'Target specific architecture patterns (comma-separated)')
30
+ .option('--arch-report', 'Generate separate architecture MD report');
31
+
26
32
  // TypeScript specific options (Phase 1 focus)
27
33
  program
28
34
  .option('--typescript', 'Enable TypeScript-specific analysis')
@@ -156,11 +162,17 @@ Advanced File Targeting:
156
162
 
157
163
  Large Project Optimization:
158
164
  $ sunlint --all --input=. --max-semantic-files=500 # Conservative analysis
159
- $ sunlint --all --input=. --max-semantic-files=2000 # Comprehensive analysis
165
+ $ sunlint --all --input=. --max-semantic-files=2000 # Comprehensive analysis
160
166
  $ sunlint --all --input=. --max-semantic-files=-1 # Unlimited (all files)
161
167
  $ sunlint --all --input=. --max-semantic-files=0 # Disable semantic analysis
162
168
  $ sunlint --all --changed-files --max-semantic-files=300 # Fast CI analysis
163
169
 
170
+ Architecture Analysis:
171
+ $ sunlint --all --architecture --input=src # Code quality + architecture
172
+ $ sunlint --architecture --input=src # Architecture only
173
+ $ sunlint --architecture --arch-report --input=src # Generate MD report
174
+ $ sunlint --architecture --arch-patterns=mvvm,layered --input=src
175
+
164
176
  Sun* Engineering - Coding Standards Made Simple ☀️
165
177
  `);
166
178
 
@@ -115,6 +115,74 @@ class OutputService {
115
115
  if (!options.quiet && options.format !== 'json') {
116
116
  console.log(report.summary);
117
117
  }
118
+
119
+ // Output architecture results if available
120
+ if (results.architecture && !options.quiet) {
121
+ this.outputArchitectureResults(results.architecture, options);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Output architecture analysis results
127
+ * @param {Object} archResults - Architecture analysis results
128
+ * @param {Object} options - Output options
129
+ */
130
+ outputArchitectureResults(archResults, options) {
131
+ if (!archResults || !archResults.summary) {
132
+ return;
133
+ }
134
+
135
+ const summary = archResults.summary;
136
+
137
+ console.log(chalk.blue('\n🏛️ Architecture Analysis:'));
138
+ console.log('━'.repeat(50));
139
+
140
+ // Primary pattern
141
+ const confidence = Math.round(summary.primaryConfidence * 100);
142
+ console.log(`• Pattern: ${chalk.cyan(summary.primaryPattern)} (${confidence}% confidence)`);
143
+
144
+ // Secondary patterns
145
+ if (summary.secondaryPatterns && summary.secondaryPatterns.length > 0) {
146
+ const secondary = summary.secondaryPatterns
147
+ .map(p => `${p.pattern} (${Math.round(p.confidence * 100)}%)`)
148
+ .join(', ');
149
+ console.log(`• Secondary: ${chalk.gray(secondary)}`);
150
+ }
151
+
152
+ // Health score
153
+ const healthScore = Math.round(summary.healthScore);
154
+ const healthColor = healthScore >= 80 ? chalk.green :
155
+ healthScore >= 60 ? chalk.yellow : chalk.red;
156
+ console.log(`• Health Score: ${healthColor(healthScore + '/100')}`);
157
+
158
+ // Violations
159
+ if (summary.violationCount > 0) {
160
+ console.log(`• Violations: ${chalk.red(summary.violationCount)}`);
161
+
162
+ // Show first 5 violations
163
+ if (archResults.violations && archResults.violations.length > 0) {
164
+ console.log(chalk.gray('\nTop Architecture Violations:'));
165
+ archResults.violations.slice(0, 5).forEach((v, i) => {
166
+ const severity = v.severity === 'error' ? chalk.red('✗') : chalk.yellow('⚠');
167
+ console.log(` ${severity} ${v.message}`);
168
+ if (v.file) {
169
+ console.log(chalk.gray(` → ${v.file}:${v.line || 1}`));
170
+ }
171
+ });
172
+ if (archResults.violations.length > 5) {
173
+ console.log(chalk.gray(` ... and ${archResults.violations.length - 5} more`));
174
+ }
175
+ }
176
+ } else {
177
+ console.log(`• Violations: ${chalk.green('None')}`);
178
+ }
179
+
180
+ // Report file info
181
+ if (archResults.markdownReport && options.archReport) {
182
+ console.log(chalk.gray('\n📄 Full architecture report saved'));
183
+ }
184
+
185
+ console.log();
118
186
  }
119
187
 
120
188
  generateAndSaveSummaryReport(violations, results, options, metadata) {
@@ -463,35 +531,30 @@ class OutputService {
463
531
  });
464
532
 
465
533
  if (uploadResult.success) {
466
- if (!options.quiet) {
467
- console.log(chalk.green(`✅ Report successfully uploaded to: ${apiUrl}`));
468
- if (uploadResult.statusCode) {
469
- console.log(chalk.green(`📡 HTTP Status: ${uploadResult.statusCode}`));
470
- }
471
- if (uploadResult.response) {
472
- try {
473
- const responseData = JSON.parse(uploadResult.response);
474
- if (responseData.message) {
475
- console.log(chalk.blue(`💬 Server response: ${responseData.message}`));
476
- }
477
- if (responseData.report_id) {
478
- console.log(chalk.blue(`📝 Report ID: ${responseData.report_id}`));
479
- }
480
- if (responseData.repository) {
481
- console.log(chalk.blue(`🏠 Repository: ${responseData.repository}`));
482
- }
483
- if (responseData.actor) {
484
- console.log(chalk.blue(`👤 Submitted by: ${responseData.actor}`));
485
- }
486
- } catch (parseError) {
487
- // If response is not JSON, show raw response
534
+ if (!options.quiet && uploadResult.response) {
535
+ try {
536
+ const responseData = JSON.parse(uploadResult.response);
537
+ if (responseData.message) {
538
+ console.log(chalk.blue(`💬 Server response: ${responseData.message}`));
539
+ }
540
+ if (responseData.report_id) {
541
+ console.log(chalk.blue(`📝 Report ID: ${responseData.report_id}`));
542
+ }
543
+ if (responseData.repository) {
544
+ console.log(chalk.blue(`🏠 Repository: ${responseData.repository}`));
545
+ }
546
+ if (responseData.actor) {
547
+ console.log(chalk.blue(`👤 Submitted by: ${responseData.actor}`));
548
+ }
549
+ } catch (parseError) {
550
+ // If response is not JSON, show raw response if verbose
551
+ if (options.verbose) {
488
552
  console.log(chalk.gray(`📄 Response: ${uploadResult.response.substring(0, 200)}...`));
489
553
  }
490
554
  }
491
555
  }
492
556
  } else {
493
- console.warn(chalk.yellow(`⚠️ Failed to upload report: ${uploadResult.error}`));
494
-
557
+ // Error already logged in upload-service.js
495
558
  if (options.verbose && uploadResult.errorContext) {
496
559
  console.warn('Upload error details:', uploadResult.errorContext);
497
560
  }