@vizzly-testing/cli 0.13.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +552 -88
  2. package/claude-plugin/.claude-plugin/README.md +4 -0
  3. package/claude-plugin/.mcp.json +4 -0
  4. package/claude-plugin/CHANGELOG.md +27 -0
  5. package/claude-plugin/mcp/vizzly-docs-server/README.md +95 -0
  6. package/claude-plugin/mcp/vizzly-docs-server/docs-fetcher.js +110 -0
  7. package/claude-plugin/mcp/vizzly-docs-server/index.js +283 -0
  8. package/claude-plugin/mcp/vizzly-server/cloud-api-provider.js +26 -10
  9. package/claude-plugin/mcp/vizzly-server/index.js +14 -1
  10. package/claude-plugin/mcp/vizzly-server/local-tdd-provider.js +61 -28
  11. package/dist/cli.js +4 -4
  12. package/dist/commands/run.js +1 -1
  13. package/dist/commands/tdd-daemon.js +54 -8
  14. package/dist/commands/tdd.js +8 -8
  15. package/dist/container/index.js +34 -3
  16. package/dist/reporter/reporter-bundle.css +1 -1
  17. package/dist/reporter/reporter-bundle.iife.js +29 -59
  18. package/dist/server/handlers/tdd-handler.js +28 -63
  19. package/dist/server/http-server.js +473 -4
  20. package/dist/services/config-service.js +371 -0
  21. package/dist/services/project-service.js +245 -0
  22. package/dist/services/server-manager.js +4 -5
  23. package/dist/services/static-report-generator.js +208 -0
  24. package/dist/services/tdd-service.js +14 -6
  25. package/dist/types/reporter/src/components/ui/form-field.d.ts +16 -0
  26. package/dist/types/reporter/src/components/views/projects-view.d.ts +1 -0
  27. package/dist/types/reporter/src/components/views/settings-view.d.ts +1 -0
  28. package/dist/types/reporter/src/hooks/use-auth.d.ts +10 -0
  29. package/dist/types/reporter/src/hooks/use-config.d.ts +9 -0
  30. package/dist/types/reporter/src/hooks/use-projects.d.ts +10 -0
  31. package/dist/types/reporter/src/services/api-client.d.ts +7 -0
  32. package/dist/types/server/http-server.d.ts +1 -1
  33. package/dist/types/services/config-service.d.ts +98 -0
  34. package/dist/types/services/project-service.d.ts +103 -0
  35. package/dist/types/services/server-manager.d.ts +2 -1
  36. package/dist/types/services/static-report-generator.d.ts +25 -0
  37. package/dist/types/services/tdd-service.d.ts +2 -2
  38. package/dist/utils/console-ui.js +26 -2
  39. package/docs/tdd-mode.md +31 -15
  40. package/package.json +4 -4
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Static Report Generator using React Reporter
3
+ * Generates a self-contained HTML file with the React dashboard and embedded data
4
+ */
5
+
6
+ import { writeFile, mkdir, copyFile } from 'fs/promises';
7
+ import { existsSync } from 'fs';
8
+ import { join, dirname } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { createServiceLogger } from '../utils/logger-factory.js';
11
+ const logger = createServiceLogger('STATIC-REPORT');
12
+ let __filename = fileURLToPath(import.meta.url);
13
+ let __dirname = dirname(__filename);
14
+ let PROJECT_ROOT = join(__dirname, '..', '..');
15
+ export class StaticReportGenerator {
16
+ constructor(workingDir, config) {
17
+ this.workingDir = workingDir;
18
+ this.config = config;
19
+ this.reportDir = join(workingDir, '.vizzly', 'report');
20
+ this.reportPath = join(this.reportDir, 'index.html');
21
+ }
22
+
23
+ /**
24
+ * Generate static HTML report with React reporter bundle
25
+ * @param {Object} reportData - Complete report data (same format as live dashboard)
26
+ * @returns {Promise<string>} Path to generated report
27
+ */
28
+ async generateReport(reportData) {
29
+ if (!reportData || typeof reportData !== 'object') {
30
+ throw new Error('Invalid report data provided');
31
+ }
32
+ try {
33
+ // Ensure report directory exists
34
+ await mkdir(this.reportDir, {
35
+ recursive: true
36
+ });
37
+
38
+ // Copy React bundles to report directory
39
+ let bundlePath = join(PROJECT_ROOT, 'dist', 'reporter', 'reporter-bundle.iife.js');
40
+ let cssPath = join(PROJECT_ROOT, 'dist', 'reporter', 'reporter-bundle.css');
41
+ if (!existsSync(bundlePath) || !existsSync(cssPath)) {
42
+ throw new Error('Reporter bundles not found. Run "npm run build:reporter" first.');
43
+ }
44
+
45
+ // Copy bundles to report directory for self-contained report
46
+ await copyFile(bundlePath, join(this.reportDir, 'reporter-bundle.js'));
47
+ await copyFile(cssPath, join(this.reportDir, 'reporter-bundle.css'));
48
+
49
+ // Generate HTML with embedded data
50
+ let htmlContent = this.generateHtmlTemplate(reportData);
51
+ await writeFile(this.reportPath, htmlContent, 'utf8');
52
+ logger.debug(`Static report generated: ${this.reportPath}`);
53
+ return this.reportPath;
54
+ } catch (error) {
55
+ logger.error(`Failed to generate static report: ${error.message}`);
56
+ throw new Error(`Report generation failed: ${error.message}`);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Generate HTML template with embedded React app
62
+ * @param {Object} reportData - Report data to embed
63
+ * @returns {string} HTML content
64
+ */
65
+ generateHtmlTemplate(reportData) {
66
+ // Serialize report data safely
67
+ let serializedData = JSON.stringify(reportData).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
68
+ return `<!DOCTYPE html>
69
+ <html lang="en">
70
+ <head>
71
+ <meta charset="UTF-8">
72
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
73
+ <title>Vizzly Dev Report - ${new Date().toLocaleString()}</title>
74
+ <link rel="stylesheet" href="./reporter-bundle.css">
75
+ <style>
76
+ /* Loading spinner styles */
77
+ .reporter-loading {
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ min-height: 100vh;
82
+ background: #0f172a;
83
+ color: #f59e0b;
84
+ }
85
+ .spinner {
86
+ width: 48px;
87
+ height: 48px;
88
+ border: 4px solid rgba(245, 158, 11, 0.2);
89
+ border-top-color: #f59e0b;
90
+ border-radius: 50%;
91
+ animation: spin 1s linear infinite;
92
+ margin-bottom: 1rem;
93
+ }
94
+ @keyframes spin {
95
+ to { transform: rotate(360deg); }
96
+ }
97
+ </style>
98
+ </head>
99
+ <body>
100
+ <div id="vizzly-reporter-root">
101
+ <div class="reporter-loading">
102
+ <div style="text-align: center;">
103
+ <div class="spinner"></div>
104
+ <p>Loading Vizzly Report...</p>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <script>
110
+ // Embedded report data (static mode)
111
+ window.VIZZLY_REPORTER_DATA = ${serializedData};
112
+ window.VIZZLY_STATIC_MODE = true;
113
+
114
+ // Generate timestamp for "generated at" display
115
+ window.VIZZLY_REPORT_GENERATED_AT = "${new Date().toISOString()}";
116
+
117
+ console.log('Vizzly Static Report loaded');
118
+ console.log('Report data:', window.VIZZLY_REPORTER_DATA?.summary);
119
+ </script>
120
+ <script src="./reporter-bundle.js"></script>
121
+ </body>
122
+ </html>`;
123
+ }
124
+
125
+ /**
126
+ * Generate a minimal HTML report when bundles are missing (fallback)
127
+ * @param {Object} reportData - Report data
128
+ * @returns {string} Minimal HTML content
129
+ */
130
+ generateFallbackHtml(reportData) {
131
+ let summary = reportData.summary || {};
132
+ let comparisons = reportData.comparisons || [];
133
+ let failed = comparisons.filter(c => c.status === 'failed');
134
+ return `<!DOCTYPE html>
135
+ <html lang="en">
136
+ <head>
137
+ <meta charset="UTF-8">
138
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
139
+ <title>Vizzly Dev Report</title>
140
+ <style>
141
+ body {
142
+ font-family: system-ui, -apple-system, sans-serif;
143
+ background: #0f172a;
144
+ color: #e2e8f0;
145
+ padding: 2rem;
146
+ }
147
+ .container { max-width: 1200px; margin: 0 auto; }
148
+ .header { text-align: center; margin-bottom: 2rem; }
149
+ .summary {
150
+ display: flex;
151
+ gap: 2rem;
152
+ justify-content: center;
153
+ margin: 2rem 0;
154
+ }
155
+ .stat { text-align: center; }
156
+ .stat-number {
157
+ font-size: 3rem;
158
+ font-weight: bold;
159
+ display: block;
160
+ }
161
+ .warning {
162
+ background: #fef3c7;
163
+ color: #92400e;
164
+ padding: 1rem;
165
+ border-radius: 0.5rem;
166
+ margin: 2rem 0;
167
+ }
168
+ </style>
169
+ </head>
170
+ <body>
171
+ <div class="container">
172
+ <div class="header">
173
+ <h1>🐻 Vizzly Dev Report</h1>
174
+ <p>Generated: ${new Date().toLocaleString()}</p>
175
+ </div>
176
+
177
+ <div class="summary">
178
+ <div class="stat">
179
+ <span class="stat-number">${summary.total || 0}</span>
180
+ <span>Total</span>
181
+ </div>
182
+ <div class="stat">
183
+ <span class="stat-number" style="color: #10b981;">${summary.passed || 0}</span>
184
+ <span>Passed</span>
185
+ </div>
186
+ <div class="stat">
187
+ <span class="stat-number" style="color: #ef4444;">${summary.failed || 0}</span>
188
+ <span>Failed</span>
189
+ </div>
190
+ </div>
191
+
192
+ <div class="warning">
193
+ <strong>⚠️ Limited Report</strong>
194
+ <p>This is a fallback report. For the full interactive experience, ensure the reporter bundle is built:</p>
195
+ <code>npm run build:reporter</code>
196
+ </div>
197
+
198
+ ${failed.length > 0 ? `
199
+ <h2>Failed Comparisons</h2>
200
+ <ul>
201
+ ${failed.map(c => `<li>${c.name} - ${c.diffPercentage || 0}% difference</li>`).join('')}
202
+ </ul>
203
+ ` : '<p style="text-align: center; font-size: 1.5rem;">✅ All tests passed!</p>'}
204
+ </div>
205
+ </body>
206
+ </html>`;
207
+ }
208
+ }
@@ -1058,14 +1058,22 @@ export class TddService {
1058
1058
 
1059
1059
  /**
1060
1060
  * Accept a current screenshot as the new baseline
1061
- * @param {string} id - Comparison ID to accept (generated from signature)
1061
+ * @param {string|Object} idOrComparison - Comparison ID or comparison object
1062
1062
  * @returns {Object} Result object
1063
1063
  */
1064
- async acceptBaseline(id) {
1065
- // Find the comparison by ID
1066
- let comparison = this.comparisons.find(c => c.id === id);
1067
- if (!comparison) {
1068
- throw new Error(`No comparison found with ID: ${id}`);
1064
+ async acceptBaseline(idOrComparison) {
1065
+ let comparison;
1066
+
1067
+ // Support both ID lookup and direct comparison object
1068
+ if (typeof idOrComparison === 'string') {
1069
+ // Find the comparison by ID in memory
1070
+ comparison = this.comparisons.find(c => c.id === idOrComparison);
1071
+ if (!comparison) {
1072
+ throw new Error(`No comparison found with ID: ${idOrComparison}`);
1073
+ }
1074
+ } else {
1075
+ // Use the provided comparison object directly
1076
+ comparison = idOrComparison;
1069
1077
  }
1070
1078
  const sanitizedName = comparison.name;
1071
1079
  let properties = comparison.properties || {};
@@ -0,0 +1,16 @@
1
+ export function FormField({ label, name, type, value, onChange, error, help, disabled, required, placeholder, options, }: {
2
+ label: any;
3
+ name: any;
4
+ type?: string;
5
+ value: any;
6
+ onChange: any;
7
+ error: any;
8
+ help: any;
9
+ disabled: any;
10
+ required: any;
11
+ placeholder: any;
12
+ options: any;
13
+ }): any;
14
+ export function ConfigSourceBadge({ source }: {
15
+ source: any;
16
+ }): any;
@@ -0,0 +1 @@
1
+ export default function ProjectsView(): any;
@@ -0,0 +1 @@
1
+ export default function SettingsView(): any;
@@ -0,0 +1,10 @@
1
+ export default function useAuth(): {
2
+ user: any;
3
+ authenticated: any;
4
+ loading: any;
5
+ error: any;
6
+ refetch: any;
7
+ initiateLogin: any;
8
+ pollAuthorization: any;
9
+ logout: any;
10
+ };
@@ -0,0 +1,9 @@
1
+ export default function useConfig(): {
2
+ config: any;
3
+ loading: any;
4
+ error: any;
5
+ saving: any;
6
+ refetch: any;
7
+ updateConfig: any;
8
+ validateConfig: any;
9
+ };
@@ -0,0 +1,10 @@
1
+ export default function useProjects(): {
2
+ projects: any;
3
+ mappings: any;
4
+ recentBuilds: any;
5
+ loading: any;
6
+ error: any;
7
+ refetch: any;
8
+ createMapping: any;
9
+ deleteMapping: any;
10
+ };
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Check if we're in static mode (data embedded in HTML)
3
+ */
4
+ export function isStaticMode(): boolean;
5
+ /**
6
+ * Fetch report data from server or return embedded static data
7
+ */
1
8
  export function fetchReportData(): Promise<any>;
2
9
  export function acceptBaseline(comparisonId: any): Promise<any>;
3
10
  export function acceptAllBaselines(): Promise<any>;
@@ -1,4 +1,4 @@
1
- export function createHttpServer(port: any, screenshotHandler: any): {
1
+ export function createHttpServer(port: any, screenshotHandler: any, services?: {}): {
2
2
  start: () => Promise<any>;
3
3
  stop: () => Promise<any>;
4
4
  finishBuild: (buildId: any) => Promise<any>;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * ConfigService for reading and writing configuration
3
+ * @extends BaseService
4
+ */
5
+ export class ConfigService extends BaseService {
6
+ constructor(config: any, options?: {});
7
+ projectRoot: any;
8
+ explorer: import("cosmiconfig").PublicExplorerSync;
9
+ /**
10
+ * Get configuration with source information
11
+ * @param {string} scope - 'project', 'global', or 'merged'
12
+ * @returns {Promise<Object>} Config object with metadata
13
+ */
14
+ getConfig(scope?: string): Promise<any>;
15
+ /**
16
+ * Get project-level config from vizzly.config.js or similar
17
+ * @private
18
+ * @returns {Promise<Object>}
19
+ */
20
+ private _getProjectConfig;
21
+ /**
22
+ * Get global config from ~/.vizzly/config.json
23
+ * @private
24
+ * @returns {Promise<Object>}
25
+ */
26
+ private _getGlobalConfig;
27
+ /**
28
+ * Get merged config showing source for each setting
29
+ * @private
30
+ * @returns {Promise<Object>}
31
+ */
32
+ private _getMergedConfig;
33
+ /**
34
+ * Update configuration
35
+ * @param {string} scope - 'project' or 'global'
36
+ * @param {Object} updates - Configuration updates to apply
37
+ * @returns {Promise<Object>} Updated config
38
+ */
39
+ updateConfig(scope: string, updates: any): Promise<any>;
40
+ /**
41
+ * Update project-level config
42
+ * @private
43
+ * @param {Object} updates - Config updates
44
+ * @returns {Promise<Object>} Updated config
45
+ */
46
+ private _updateProjectConfig;
47
+ /**
48
+ * Update global config
49
+ * @private
50
+ * @param {Object} updates - Config updates
51
+ * @returns {Promise<Object>} Updated config
52
+ */
53
+ private _updateGlobalConfig;
54
+ /**
55
+ * Write project config file (JavaScript format)
56
+ * @private
57
+ * @param {string} filepath - Path to write to
58
+ * @param {Object} config - Config object
59
+ * @returns {Promise<void>}
60
+ */
61
+ private _writeProjectConfigFile;
62
+ /**
63
+ * Serialize config object to JavaScript module
64
+ * @private
65
+ * @param {Object} config - Config object
66
+ * @returns {string} JavaScript source code
67
+ */
68
+ private _serializeToJavaScript;
69
+ /**
70
+ * Stringify object with proper indentation (2 spaces)
71
+ * @private
72
+ * @param {*} value - Value to stringify
73
+ * @param {number} depth - Current depth
74
+ * @returns {string}
75
+ */
76
+ private _stringifyWithIndent;
77
+ /**
78
+ * Validate configuration object
79
+ * @param {Object} config - Config to validate
80
+ * @returns {Promise<Object>} Validation result
81
+ */
82
+ validateConfig(config: any): Promise<any>;
83
+ /**
84
+ * Get the source of a specific config key
85
+ * @param {string} key - Config key
86
+ * @returns {Promise<string>} Source ('default', 'global', 'project', 'env', 'cli')
87
+ */
88
+ getConfigSource(key: string): Promise<string>;
89
+ /**
90
+ * Deep merge two objects
91
+ * @private
92
+ * @param {Object} target - Target object
93
+ * @param {Object} source - Source object
94
+ * @returns {Object} Merged object
95
+ */
96
+ private _deepMerge;
97
+ }
98
+ import { BaseService } from './base-service.js';
@@ -0,0 +1,103 @@
1
+ /**
2
+ * ProjectService for managing project mappings and operations
3
+ * @extends BaseService
4
+ */
5
+ export class ProjectService extends BaseService {
6
+ constructor(config: any, options?: {});
7
+ apiService: any;
8
+ /**
9
+ * List all project mappings
10
+ * @returns {Promise<Array>} Array of project mappings
11
+ */
12
+ listMappings(): Promise<any[]>;
13
+ /**
14
+ * Get project mapping for a specific directory
15
+ * @param {string} directory - Directory path
16
+ * @returns {Promise<Object|null>} Project mapping or null
17
+ */
18
+ getMapping(directory: string): Promise<any | null>;
19
+ /**
20
+ * Create or update project mapping
21
+ * @param {string} directory - Directory path
22
+ * @param {Object} projectData - Project data
23
+ * @param {string} projectData.projectSlug - Project slug
24
+ * @param {string} projectData.organizationSlug - Organization slug
25
+ * @param {string} projectData.token - Project API token
26
+ * @param {string} [projectData.projectName] - Optional project name
27
+ * @returns {Promise<Object>} Created mapping
28
+ */
29
+ createMapping(directory: string, projectData: {
30
+ projectSlug: string;
31
+ organizationSlug: string;
32
+ token: string;
33
+ projectName?: string;
34
+ }): Promise<any>;
35
+ /**
36
+ * Remove project mapping
37
+ * @param {string} directory - Directory path
38
+ * @returns {Promise<void>}
39
+ */
40
+ removeMapping(directory: string): Promise<void>;
41
+ /**
42
+ * Switch project for current directory
43
+ * @param {string} projectSlug - Project slug
44
+ * @param {string} organizationSlug - Organization slug
45
+ * @param {string} token - Project token
46
+ * @returns {Promise<Object>} Updated mapping
47
+ */
48
+ switchProject(projectSlug: string, organizationSlug: string, token: string): Promise<any>;
49
+ /**
50
+ * List all projects from API
51
+ * @returns {Promise<Array>} Array of projects
52
+ */
53
+ listProjects(): Promise<any[]>;
54
+ /**
55
+ * Get project details
56
+ * @param {string} projectSlug - Project slug
57
+ * @param {string} organizationSlug - Organization slug
58
+ * @returns {Promise<Object>} Project details
59
+ */
60
+ getProject(projectSlug: string, organizationSlug: string): Promise<any>;
61
+ /**
62
+ * Get recent builds for a project
63
+ * @param {string} projectSlug - Project slug
64
+ * @param {string} organizationSlug - Organization slug
65
+ * @param {Object} options - Query options
66
+ * @param {number} [options.limit=10] - Number of builds to fetch
67
+ * @param {string} [options.branch] - Filter by branch
68
+ * @returns {Promise<Array>} Array of builds
69
+ */
70
+ getRecentBuilds(projectSlug: string, organizationSlug: string, options?: {
71
+ limit?: number;
72
+ branch?: string;
73
+ }): Promise<any[]>;
74
+ /**
75
+ * Create a project token
76
+ * @param {string} projectSlug - Project slug
77
+ * @param {string} organizationSlug - Organization slug
78
+ * @param {Object} tokenData - Token data
79
+ * @param {string} tokenData.name - Token name
80
+ * @param {string} [tokenData.description] - Token description
81
+ * @returns {Promise<Object>} Created token
82
+ */
83
+ createProjectToken(projectSlug: string, organizationSlug: string, tokenData: {
84
+ name: string;
85
+ description?: string;
86
+ }): Promise<any>;
87
+ /**
88
+ * List project tokens
89
+ * @param {string} projectSlug - Project slug
90
+ * @param {string} organizationSlug - Organization slug
91
+ * @returns {Promise<Array>} Array of tokens
92
+ */
93
+ listProjectTokens(projectSlug: string, organizationSlug: string): Promise<any[]>;
94
+ /**
95
+ * Revoke a project token
96
+ * @param {string} projectSlug - Project slug
97
+ * @param {string} organizationSlug - Organization slug
98
+ * @param {string} tokenId - Token ID
99
+ * @returns {Promise<void>}
100
+ */
101
+ revokeProjectToken(projectSlug: string, organizationSlug: string, tokenId: string): Promise<void>;
102
+ }
103
+ import { BaseService } from './base-service.js';
@@ -1,5 +1,5 @@
1
1
  export class ServerManager extends BaseService {
2
- constructor(config: any, logger: any);
2
+ constructor(config: any, options?: {});
3
3
  httpServer: {
4
4
  start: () => Promise<any>;
5
5
  stop: () => Promise<any>;
@@ -253,6 +253,7 @@ export class ServerManager extends BaseService {
253
253
  }>;
254
254
  cleanup: () => void;
255
255
  };
256
+ services: any;
256
257
  start(buildId?: any, tddMode?: boolean, setBaseline?: boolean): Promise<void>;
257
258
  buildId: any;
258
259
  tddMode: boolean;
@@ -0,0 +1,25 @@
1
+ export class StaticReportGenerator {
2
+ constructor(workingDir: any, config: any);
3
+ workingDir: any;
4
+ config: any;
5
+ reportDir: any;
6
+ reportPath: any;
7
+ /**
8
+ * Generate static HTML report with React reporter bundle
9
+ * @param {Object} reportData - Complete report data (same format as live dashboard)
10
+ * @returns {Promise<string>} Path to generated report
11
+ */
12
+ generateReport(reportData: any): Promise<string>;
13
+ /**
14
+ * Generate HTML template with embedded React app
15
+ * @param {Object} reportData - Report data to embed
16
+ * @returns {string} HTML content
17
+ */
18
+ generateHtmlTemplate(reportData: any): string;
19
+ /**
20
+ * Generate a minimal HTML report when bundles are missing (fallback)
21
+ * @param {Object} reportData - Report data
22
+ * @returns {string} Minimal HTML content
23
+ */
24
+ generateFallbackHtml(reportData: any): string;
25
+ }
@@ -76,9 +76,9 @@ export class TddService {
76
76
  private updateSingleBaseline;
77
77
  /**
78
78
  * Accept a current screenshot as the new baseline
79
- * @param {string} id - Comparison ID to accept (generated from signature)
79
+ * @param {string|Object} idOrComparison - Comparison ID or comparison object
80
80
  * @returns {Object} Result object
81
81
  */
82
- acceptBaseline(id: string): any;
82
+ acceptBaseline(idOrComparison: string | any): any;
83
83
  }
84
84
  import { ApiService } from '../services/api-service.js';
@@ -47,6 +47,7 @@ export class ConsoleUI {
47
47
  errorData.error = {
48
48
  name: error.name,
49
49
  message: errorMessage,
50
+ code: error.code,
50
51
  ...(this.verbose && {
51
52
  stack: error.stack
52
53
  })
@@ -57,8 +58,31 @@ export class ConsoleUI {
57
58
  console.error(JSON.stringify(errorData));
58
59
  } else {
59
60
  console.error(this.colors.red(`✖ ${message}`));
60
- if (this.verbose && error.stack) {
61
- console.error(this.colors.dim(error.stack));
61
+
62
+ // Always show error details (not just in verbose mode)
63
+ if (error instanceof Error) {
64
+ // Use getUserMessage() if available (VizzlyError classes), otherwise use message
65
+ const errorMessage = error.getUserMessage ? error.getUserMessage() : error.message;
66
+ if (errorMessage && errorMessage !== message) {
67
+ console.error(this.colors.dim(errorMessage));
68
+ }
69
+ // Stack traces only in verbose mode
70
+ if (this.verbose && error.stack) {
71
+ console.error(this.colors.dim(error.stack));
72
+ }
73
+ } else if (typeof error === 'string' && error) {
74
+ console.error(this.colors.dim(error));
75
+ } else if (error && typeof error === 'object') {
76
+ // Show error object details if available
77
+ try {
78
+ const errorStr = JSON.stringify(error, null, 2);
79
+ if (errorStr !== '{}') {
80
+ console.error(this.colors.dim(errorStr));
81
+ }
82
+ } catch {
83
+ // Fallback for circular references or other JSON.stringify errors
84
+ console.error(this.colors.dim(String(error)));
85
+ }
62
86
  }
63
87
  }
64
88
  if (exitCode > 0) {