@vizzly-testing/cli 0.7.2 → 0.9.0

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 (77) hide show
  1. package/README.md +27 -14
  2. package/dist/cli.js +25 -1
  3. package/dist/client/index.js +77 -11
  4. package/dist/commands/init.js +23 -17
  5. package/dist/commands/tdd-daemon.js +312 -0
  6. package/dist/commands/tdd.js +45 -14
  7. package/dist/commands/upload.js +3 -1
  8. package/dist/reporter/reporter-bundle.css +1 -0
  9. package/dist/reporter/reporter-bundle.iife.js +57 -0
  10. package/dist/sdk/index.js +1 -1
  11. package/dist/server/handlers/api-handler.js +98 -30
  12. package/dist/server/handlers/tdd-handler.js +264 -77
  13. package/dist/server/http-server.js +358 -15
  14. package/dist/services/api-service.js +6 -1
  15. package/dist/services/html-report-generator.js +77 -0
  16. package/dist/services/report-generator/report.css +56 -0
  17. package/dist/services/screenshot-server.js +6 -3
  18. package/dist/services/server-manager.js +2 -9
  19. package/dist/services/tdd-service.js +188 -25
  20. package/dist/services/test-runner.js +43 -1
  21. package/dist/types/commands/tdd-daemon.d.ts +18 -0
  22. package/dist/types/container/index.d.ts +1 -3
  23. package/dist/types/reporter/src/components/app-router.d.ts +3 -0
  24. package/dist/types/reporter/src/components/comparison/comparison-actions.d.ts +5 -0
  25. package/dist/types/reporter/src/components/comparison/comparison-card.d.ts +6 -0
  26. package/dist/types/reporter/src/components/comparison/comparison-list.d.ts +6 -0
  27. package/dist/types/reporter/src/components/comparison/comparison-viewer.d.ts +4 -0
  28. package/dist/types/reporter/src/components/comparison/view-mode-selector.d.ts +4 -0
  29. package/dist/types/reporter/src/components/comparison/viewer-modes/onion-viewer.d.ts +3 -0
  30. package/dist/types/reporter/src/components/comparison/viewer-modes/overlay-viewer.d.ts +3 -0
  31. package/dist/types/reporter/src/components/comparison/viewer-modes/side-by-side-viewer.d.ts +3 -0
  32. package/dist/types/reporter/src/components/comparison/viewer-modes/toggle-viewer.d.ts +3 -0
  33. package/dist/types/reporter/src/components/dashboard/dashboard-filters.d.ts +16 -0
  34. package/dist/types/reporter/src/components/dashboard/dashboard-header.d.ts +5 -0
  35. package/dist/types/reporter/src/components/dashboard/dashboard-stats.d.ts +4 -0
  36. package/dist/types/reporter/src/components/dashboard/empty-state.d.ts +8 -0
  37. package/dist/types/reporter/src/components/ui/smart-image.d.ts +7 -0
  38. package/dist/types/reporter/src/components/ui/status-badge.d.ts +5 -0
  39. package/dist/types/reporter/src/components/ui/toast.d.ts +4 -0
  40. package/dist/types/reporter/src/components/views/comparisons-view.d.ts +6 -0
  41. package/dist/types/reporter/src/components/views/stats-view.d.ts +6 -0
  42. package/dist/types/reporter/src/hooks/use-baseline-actions.d.ts +5 -0
  43. package/dist/types/reporter/src/hooks/use-comparison-filters.d.ts +20 -0
  44. package/dist/types/reporter/src/hooks/use-image-loader.d.ts +1 -0
  45. package/dist/types/reporter/src/hooks/use-report-data.d.ts +7 -0
  46. package/dist/types/reporter/src/hooks/use-vizzly-api.d.ts +9 -0
  47. package/dist/types/reporter/src/main.d.ts +1 -0
  48. package/dist/types/reporter/src/services/api-client.d.ts +4 -0
  49. package/dist/types/reporter/src/utils/comparison-helpers.d.ts +16 -0
  50. package/dist/types/reporter/src/utils/constants.d.ts +37 -0
  51. package/dist/types/reporter/vite.config.d.ts +2 -0
  52. package/dist/types/reporter/vite.dev.config.d.ts +2 -0
  53. package/dist/types/sdk/index.d.ts +2 -3
  54. package/dist/types/server/handlers/api-handler.d.ts +5 -14
  55. package/dist/types/server/handlers/tdd-handler.d.ts +18 -17
  56. package/dist/types/server/http-server.d.ts +2 -1
  57. package/dist/types/services/base-service.d.ts +1 -2
  58. package/dist/types/services/html-report-generator.d.ts +3 -3
  59. package/dist/types/services/screenshot-server.d.ts +1 -1
  60. package/dist/types/services/server-manager.d.ts +25 -35
  61. package/dist/types/services/tdd-service.d.ts +7 -1
  62. package/dist/types/services/test-runner.d.ts +6 -1
  63. package/dist/types/utils/build-history.d.ts +16 -0
  64. package/dist/types/utils/config-loader.d.ts +1 -1
  65. package/dist/types/utils/console-ui.d.ts +1 -1
  66. package/dist/types/utils/git.d.ts +4 -4
  67. package/dist/types/utils/security.d.ts +2 -1
  68. package/dist/utils/build-history.js +103 -0
  69. package/dist/utils/config-loader.js +1 -1
  70. package/dist/utils/console-ui.js +2 -1
  71. package/dist/utils/environment-config.js +1 -1
  72. package/dist/utils/security.js +14 -5
  73. package/docs/api-reference.md +2 -4
  74. package/docs/doctor-command.md +1 -1
  75. package/docs/getting-started.md +1 -1
  76. package/docs/tdd-mode.md +176 -112
  77. package/package.json +17 -4
@@ -0,0 +1,37 @@
1
+ export namespace VIEW_MODES {
2
+ let OVERLAY: string;
3
+ let TOGGLE: string;
4
+ let ONION: string;
5
+ let SIDE_BY_SIDE: string;
6
+ }
7
+ export namespace FILTER_TYPES {
8
+ let ALL: string;
9
+ let FAILED: string;
10
+ let PASSED: string;
11
+ let NEW: string;
12
+ }
13
+ export namespace SORT_TYPES {
14
+ let PRIORITY: string;
15
+ let NAME: string;
16
+ let TIME: string;
17
+ }
18
+ export namespace CONNECTION_STATUS {
19
+ let CONNECTING: string;
20
+ let CONNECTED: string;
21
+ let DISCONNECTED: string;
22
+ }
23
+ export namespace COMPARISON_STATUS {
24
+ let PASSED_1: string;
25
+ export { PASSED_1 as PASSED };
26
+ let FAILED_1: string;
27
+ export { FAILED_1 as FAILED };
28
+ let NEW_1: string;
29
+ export { NEW_1 as NEW };
30
+ export let BASELINE_CREATED: string;
31
+ export let ERROR: string;
32
+ }
33
+ export namespace USER_ACTION {
34
+ let ACCEPTING: string;
35
+ let ACCEPTED: string;
36
+ let REJECTED: string;
37
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * const vizzly = await createVizzly({
12
12
  * apiKey: process.env.VIZZLY_TOKEN,
13
- * apiUrl: 'https://vizzly.dev',
13
+ * apiUrl: 'https://app.vizzly.dev',
14
14
  * server: {
15
15
  * port: 3003,
16
16
  * enabled: true
@@ -48,7 +48,7 @@ export function createVizzly(config?: any, options?: {}): Promise<VizzlySDK>;
48
48
  * @class
49
49
  * @extends {EventEmitter}
50
50
  */
51
- export class VizzlySDK extends EventEmitter<[never]> {
51
+ export class VizzlySDK {
52
52
  /**
53
53
  * @param {import('../types').VizzlyConfig} config - Configuration
54
54
  * @param {import('../utils/logger').Logger} logger - Logger instance
@@ -104,5 +104,4 @@ export { loadConfig } from "../utils/config-loader.js";
104
104
  export { createLogger } from "../utils/logger.js";
105
105
  export { createUploader } from "../services/uploader.js";
106
106
  export { createTDDService } from "../services/tdd-service.js";
107
- import { EventEmitter } from 'events';
108
107
  import { ScreenshotServer } from '../services/screenshot-server.js';
@@ -8,7 +8,6 @@ export function createApiHandler(apiService: any): {
8
8
  message: string;
9
9
  error?: undefined;
10
10
  name?: undefined;
11
- skipped?: undefined;
12
11
  };
13
12
  } | {
14
13
  statusCode: number;
@@ -19,31 +18,23 @@ export function createApiHandler(apiService: any): {
19
18
  count?: undefined;
20
19
  message?: undefined;
21
20
  name?: undefined;
22
- skipped?: undefined;
23
21
  };
24
22
  } | {
25
23
  statusCode: number;
26
24
  body: {
27
25
  success: boolean;
28
26
  name: any;
29
- skipped: any;
30
27
  count: number;
31
28
  disabled?: undefined;
32
29
  message?: undefined;
33
30
  error?: undefined;
34
31
  };
35
- } | {
36
- statusCode: number;
37
- body: {
38
- success: boolean;
39
- name: any;
40
- disabled: boolean;
41
- message: string;
42
- count?: undefined;
43
- error?: undefined;
44
- skipped?: undefined;
45
- };
46
32
  }>;
47
33
  getScreenshotCount: () => number;
34
+ flush: () => Promise<{
35
+ uploaded: number;
36
+ failed: number;
37
+ total: number;
38
+ }>;
48
39
  cleanup: () => void;
49
40
  };
@@ -1,6 +1,5 @@
1
1
  export function createTddHandler(config: any, workingDir: any, baselineBuild: any, baselineComparison: any, setBaseline?: boolean): {
2
2
  initialize: () => Promise<void>;
3
- registerBuild: (buildId: any) => void;
4
3
  handleScreenshot: (buildId: any, name: any, image: any, properties?: {}) => Promise<{
5
4
  statusCode: number;
6
5
  body: {
@@ -81,22 +80,24 @@ export function createTddHandler(config: any, workingDir: any, baselineBuild: an
81
80
  message?: undefined;
82
81
  };
83
82
  }>;
84
- getScreenshotCount: (buildId: any) => any;
85
- finishBuild: (buildId: any) => Promise<{
86
- id: any;
87
- name: any;
88
- tddMode: boolean;
89
- results: {
90
- total: number;
91
- passed: number;
92
- failed: number;
93
- new: number;
94
- errors: number;
95
- comparisons: any[];
96
- baseline: any;
97
- };
98
- url: any;
99
- passed: boolean;
83
+ getResults: () => Promise<{
84
+ total: number;
85
+ passed: number;
86
+ failed: number;
87
+ new: number;
88
+ errors: number;
89
+ comparisons: any[];
90
+ baseline: any;
91
+ }>;
92
+ acceptBaseline: (screenshotName: any) => Promise<any>;
93
+ acceptAllBaselines: () => Promise<{
94
+ count: number;
95
+ }>;
96
+ resetBaselines: () => Promise<{
97
+ success: boolean;
98
+ deletedBaselines: number;
99
+ deletedCurrents: number;
100
+ deletedDiffs: number;
100
101
  }>;
101
102
  cleanup: () => void;
102
103
  };
@@ -1,5 +1,6 @@
1
- export function createHttpServer(port: any, screenshotHandler: any, emitter?: any): {
1
+ export function createHttpServer(port: any, screenshotHandler: any): {
2
2
  start: () => Promise<any>;
3
3
  stop: () => Promise<any>;
4
+ finishBuild: (buildId: any) => Promise<any>;
4
5
  getServer: () => any;
5
6
  };
@@ -7,7 +7,7 @@
7
7
  * Base class for all services
8
8
  * @extends EventEmitter
9
9
  */
10
- export class BaseService extends EventEmitter<[never]> {
10
+ export class BaseService {
11
11
  /**
12
12
  * @param {Object} config - Service configuration
13
13
  * @param {ServiceOptions} options - Service options
@@ -69,4 +69,3 @@ export type ServiceOptions = {
69
69
  */
70
70
  signal?: AbortSignal;
71
71
  };
72
- import { EventEmitter } from 'events';
@@ -2,9 +2,9 @@ export class HtmlReportGenerator {
2
2
  constructor(workingDir: any, config: any);
3
3
  workingDir: any;
4
4
  config: any;
5
- reportDir: string;
6
- reportPath: string;
7
- cssPath: string;
5
+ reportDir: any;
6
+ reportPath: any;
7
+ cssPath: any;
8
8
  /**
9
9
  * Sanitize HTML content to prevent XSS attacks
10
10
  * @param {string} text - Text to sanitize
@@ -1,7 +1,7 @@
1
1
  export class ScreenshotServer extends BaseService {
2
2
  constructor(config: any, logger: any, buildManager: any);
3
3
  buildManager: any;
4
- server: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
4
+ server: any;
5
5
  onStart(): Promise<any>;
6
6
  onStop(): Promise<any>;
7
7
  handleRequest(req: any, res: any): Promise<void>;
@@ -3,11 +3,11 @@ export class ServerManager extends BaseService {
3
3
  httpServer: {
4
4
  start: () => Promise<any>;
5
5
  stop: () => Promise<any>;
6
+ finishBuild: (buildId: any) => Promise<any>;
6
7
  getServer: () => any;
7
8
  };
8
9
  handler: {
9
10
  initialize: () => Promise<void>;
10
- registerBuild: (buildId: any) => void;
11
11
  handleScreenshot: (buildId: any, name: any, image: any, properties?: {}) => Promise<{
12
12
  statusCode: number;
13
13
  body: {
@@ -88,22 +88,24 @@ export class ServerManager extends BaseService {
88
88
  message?: undefined;
89
89
  };
90
90
  }>;
91
- getScreenshotCount: (buildId: any) => any;
92
- finishBuild: (buildId: any) => Promise<{
93
- id: any;
94
- name: any;
95
- tddMode: boolean;
96
- results: {
97
- total: number;
98
- passed: number;
99
- failed: number;
100
- new: number;
101
- errors: number;
102
- comparisons: any[];
103
- baseline: any;
104
- };
105
- url: any;
106
- passed: boolean;
91
+ getResults: () => Promise<{
92
+ total: number;
93
+ passed: number;
94
+ failed: number;
95
+ new: number;
96
+ errors: number;
97
+ comparisons: any[];
98
+ baseline: any;
99
+ }>;
100
+ acceptBaseline: (screenshotName: any) => Promise<any>;
101
+ acceptAllBaselines: () => Promise<{
102
+ count: number;
103
+ }>;
104
+ resetBaselines: () => Promise<{
105
+ success: boolean;
106
+ deletedBaselines: number;
107
+ deletedCurrents: number;
108
+ deletedDiffs: number;
107
109
  }>;
108
110
  cleanup: () => void;
109
111
  } | {
@@ -116,7 +118,6 @@ export class ServerManager extends BaseService {
116
118
  message: string;
117
119
  error?: undefined;
118
120
  name?: undefined;
119
- skipped?: undefined;
120
121
  };
121
122
  } | {
122
123
  statusCode: number;
@@ -127,45 +128,34 @@ export class ServerManager extends BaseService {
127
128
  count?: undefined;
128
129
  message?: undefined;
129
130
  name?: undefined;
130
- skipped?: undefined;
131
131
  };
132
132
  } | {
133
133
  statusCode: number;
134
134
  body: {
135
135
  success: boolean;
136
136
  name: any;
137
- skipped: any;
138
137
  count: number;
139
138
  disabled?: undefined;
140
139
  message?: undefined;
141
140
  error?: undefined;
142
141
  };
143
- } | {
144
- statusCode: number;
145
- body: {
146
- success: boolean;
147
- name: any;
148
- disabled: boolean;
149
- message: string;
150
- count?: undefined;
151
- error?: undefined;
152
- skipped?: undefined;
153
- };
154
142
  }>;
155
143
  getScreenshotCount: () => number;
144
+ flush: () => Promise<{
145
+ uploaded: number;
146
+ failed: number;
147
+ total: number;
148
+ }>;
156
149
  cleanup: () => void;
157
150
  };
158
- emitter: EventEmitter<[never]>;
159
151
  start(buildId?: any, tddMode?: boolean, setBaseline?: boolean): Promise<void>;
160
152
  buildId: any;
161
153
  tddMode: boolean;
162
154
  setBaseline: boolean;
163
155
  createApiService(): Promise<import("./api-service.js").ApiService>;
164
156
  get server(): {
165
- emitter: EventEmitter<[never]>;
166
157
  getScreenshotCount: (buildId: any) => any;
167
- finishBuild: (buildId: any) => any;
158
+ finishBuild: (buildId: any) => Promise<any>;
168
159
  };
169
160
  }
170
161
  import { BaseService } from './base-service.js';
171
- import { EventEmitter } from 'events';
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export function createTDDService(config: any, options?: {}): TddService;
5
5
  export class TddService {
6
- constructor(config: any, workingDir?: string, setBaseline?: boolean);
6
+ constructor(config: any, workingDir?: any, setBaseline?: boolean);
7
7
  config: any;
8
8
  setBaseline: boolean;
9
9
  api: ApiService;
@@ -72,5 +72,11 @@ export class TddService {
72
72
  * @private
73
73
  */
74
74
  private updateSingleBaseline;
75
+ /**
76
+ * Accept a current screenshot as the new baseline
77
+ * @param {string} name - Screenshot name to accept
78
+ * @returns {Object} Result object
79
+ */
80
+ acceptBaseline(name: string): any;
75
81
  }
76
82
  import { ApiService } from '../services/api-service.js';
@@ -3,7 +3,12 @@ export class TestRunner extends BaseService {
3
3
  buildManager: any;
4
4
  serverManager: any;
5
5
  tddService: any;
6
- testProcess: import("child_process").ChildProcess;
6
+ testProcess: any;
7
+ /**
8
+ * Initialize server for daemon mode (no test execution)
9
+ * @param {Object} options - Options for server initialization
10
+ */
11
+ initialize(options: any): Promise<void>;
7
12
  run(options: any): Promise<{
8
13
  testsPassed: number;
9
14
  testsFailed: number;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Archive a build to history directory
3
+ * @param {string} workingDir - Working directory
4
+ * @param {string} buildId - Build ID to archive
5
+ * @param {Array} builds - Build data
6
+ * @param {Array} comparisons - Comparison data
7
+ * @param {Object} summary - Summary stats
8
+ * @param {number} maxHistory - Maximum number of builds to keep (default: 3)
9
+ */
10
+ export function archiveBuild(workingDir: string, buildId: string, builds: any[], comparisons: any[], summary: any, maxHistory?: number): void;
11
+ /**
12
+ * Get list of archived builds
13
+ * @param {string} workingDir - Working directory
14
+ * @returns {Array} Array of build metadata
15
+ */
16
+ export function getArchivedBuilds(workingDir: string): any[];
@@ -22,4 +22,4 @@ export function loadConfig(configPath?: any, cliOverrides?: {}): Promise<{
22
22
  apiKey: string;
23
23
  apiUrl: string;
24
24
  }>;
25
- export function getScreenshotPaths(config: any): string[];
25
+ export function getScreenshotPaths(config: any): any[];
@@ -11,7 +11,7 @@ export class ConsoleUI {
11
11
  };
12
12
  json: any;
13
13
  verbose: any;
14
- spinner: NodeJS.Timeout;
14
+ spinner: number;
15
15
  lastLine: string;
16
16
  /**
17
17
  * Show a success message
@@ -1,7 +1,7 @@
1
- export function getCommonAncestor(commit1: any, commit2: any, cwd?: string): Promise<string>;
2
- export function getCurrentCommitSha(cwd?: string): Promise<string>;
3
- export function getCurrentBranch(cwd?: string): Promise<string>;
4
- export function getDefaultBranch(cwd?: string): Promise<string>;
1
+ export function getCommonAncestor(commit1: any, commit2: any, cwd?: any): Promise<any>;
2
+ export function getCurrentCommitSha(cwd?: any): Promise<any>;
3
+ export function getCurrentBranch(cwd?: any): Promise<any>;
4
+ export function getDefaultBranch(cwd?: any): Promise<any>;
5
5
  export function generateBuildName(): string;
6
6
  /**
7
7
  * Get the current commit message
@@ -2,9 +2,10 @@
2
2
  * Sanitizes a screenshot name to prevent path traversal and ensure safe file naming
3
3
  * @param {string} name - Original screenshot name
4
4
  * @param {number} maxLength - Maximum allowed length (default: 255)
5
+ * @param {boolean} allowSlashes - Whether to allow forward slashes (for browser version strings)
5
6
  * @returns {string} Sanitized screenshot name
6
7
  */
7
- export function sanitizeScreenshotName(name: string, maxLength?: number): string;
8
+ export function sanitizeScreenshotName(name: string, maxLength?: number, allowSlashes?: boolean): string;
8
9
  /**
9
10
  * Validates that a path stays within the allowed working directory bounds
10
11
  * @param {string} targetPath - Path to validate
@@ -0,0 +1,103 @@
1
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, rmSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ /**
5
+ * Archive a build to history directory
6
+ * @param {string} workingDir - Working directory
7
+ * @param {string} buildId - Build ID to archive
8
+ * @param {Array} builds - Build data
9
+ * @param {Array} comparisons - Comparison data
10
+ * @param {Object} summary - Summary stats
11
+ * @param {number} maxHistory - Maximum number of builds to keep (default: 3)
12
+ */
13
+ export function archiveBuild(workingDir, buildId, builds, comparisons, summary, maxHistory = 3) {
14
+ let historyDir = join(workingDir, '.vizzly', 'history');
15
+
16
+ // Create history directory if it doesn't exist
17
+ if (!existsSync(historyDir)) {
18
+ mkdirSync(historyDir, {
19
+ recursive: true
20
+ });
21
+ }
22
+
23
+ // Save current build to history
24
+ let buildDir = join(historyDir, buildId);
25
+ if (!existsSync(buildDir)) {
26
+ mkdirSync(buildDir, {
27
+ recursive: true
28
+ });
29
+ }
30
+ let buildData = {
31
+ buildId,
32
+ timestamp: Date.now(),
33
+ builds,
34
+ comparisons,
35
+ summary
36
+ };
37
+ writeFileSync(join(buildDir, 'report.json'), JSON.stringify(buildData, null, 2));
38
+
39
+ // Clean up old builds (keep last N)
40
+ cleanupOldBuilds(historyDir, maxHistory);
41
+ }
42
+
43
+ /**
44
+ * Get list of archived builds
45
+ * @param {string} workingDir - Working directory
46
+ * @returns {Array} Array of build metadata
47
+ */
48
+ export function getArchivedBuilds(workingDir) {
49
+ let historyDir = join(workingDir, '.vizzly', 'history');
50
+ if (!existsSync(historyDir)) {
51
+ return [];
52
+ }
53
+ try {
54
+ let buildDirs = readdirSync(historyDir, {
55
+ withFileTypes: true
56
+ }).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name).sort().reverse(); // Newest first
57
+
58
+ return buildDirs.map(buildId => {
59
+ let reportPath = join(historyDir, buildId, 'report.json');
60
+ if (existsSync(reportPath)) {
61
+ try {
62
+ let data = JSON.parse(require('fs').readFileSync(reportPath, 'utf8'));
63
+ return {
64
+ buildId: data.buildId,
65
+ timestamp: data.timestamp,
66
+ summary: data.summary
67
+ };
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ return null;
73
+ }).filter(Boolean);
74
+ } catch {
75
+ return [];
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Remove old builds, keeping only the last N
81
+ * @private
82
+ */
83
+ function cleanupOldBuilds(historyDir, maxHistory) {
84
+ try {
85
+ let buildDirs = readdirSync(historyDir, {
86
+ withFileTypes: true
87
+ }).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name).sort().reverse(); // Newest first
88
+
89
+ // Remove builds beyond maxHistory
90
+ if (buildDirs.length > maxHistory) {
91
+ let toRemove = buildDirs.slice(maxHistory);
92
+ toRemove.forEach(buildId => {
93
+ let buildDir = join(historyDir, buildId);
94
+ rmSync(buildDir, {
95
+ recursive: true,
96
+ force: true
97
+ });
98
+ });
99
+ }
100
+ } catch {
101
+ // Ignore cleanup errors
102
+ }
103
+ }
@@ -64,7 +64,7 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
64
64
  const envApiUrl = getApiUrl();
65
65
  const envParallelId = getParallelId();
66
66
  if (envApiKey) config.apiKey = envApiKey;
67
- if (envApiUrl !== 'https://vizzly.dev') config.apiUrl = envApiUrl;
67
+ if (envApiUrl !== 'https://app.vizzly.dev') config.apiUrl = envApiUrl;
68
68
  if (envParallelId) config.parallelId = envParallelId;
69
69
 
70
70
  // 3. Apply CLI overrides (highest priority)
@@ -43,9 +43,10 @@ export class ConsoleUI {
43
43
  timestamp: new Date().toISOString()
44
44
  };
45
45
  if (error instanceof Error) {
46
+ const errorMessage = error.getUserMessage ? error.getUserMessage() : error.message;
46
47
  errorData.error = {
47
48
  name: error.name,
48
- message: error.message,
49
+ message: errorMessage,
49
50
  ...(this.verbose && {
50
51
  stack: error.stack
51
52
  })
@@ -16,7 +16,7 @@ export function getApiToken() {
16
16
  * @returns {string} API URL with default
17
17
  */
18
18
  export function getApiUrl() {
19
- return process.env.VIZZLY_API_URL || 'https://vizzly.dev';
19
+ return process.env.VIZZLY_API_URL || 'https://app.vizzly.dev';
20
20
  }
21
21
 
22
22
  /**
@@ -11,9 +11,10 @@ const logger = createServiceLogger('SECURITY');
11
11
  * Sanitizes a screenshot name to prevent path traversal and ensure safe file naming
12
12
  * @param {string} name - Original screenshot name
13
13
  * @param {number} maxLength - Maximum allowed length (default: 255)
14
+ * @param {boolean} allowSlashes - Whether to allow forward slashes (for browser version strings)
14
15
  * @returns {string} Sanitized screenshot name
15
16
  */
16
- export function sanitizeScreenshotName(name, maxLength = 255) {
17
+ export function sanitizeScreenshotName(name, maxLength = 255, allowSlashes = false) {
17
18
  if (typeof name !== 'string' || name.length === 0) {
18
19
  throw new Error('Screenshot name must be a non-empty string');
19
20
  }
@@ -22,7 +23,12 @@ export function sanitizeScreenshotName(name, maxLength = 255) {
22
23
  }
23
24
 
24
25
  // Block directory traversal patterns
25
- if (name.includes('..') || name.includes('/') || name.includes('\\')) {
26
+ if (name.includes('..') || name.includes('\\')) {
27
+ throw new Error('Screenshot name contains invalid path characters');
28
+ }
29
+
30
+ // Block forward slashes unless explicitly allowed (e.g., for browser version strings)
31
+ if (!allowSlashes && name.includes('/')) {
26
32
  throw new Error('Screenshot name contains invalid path characters');
27
33
  }
28
34
 
@@ -31,9 +37,10 @@ export function sanitizeScreenshotName(name, maxLength = 255) {
31
37
  throw new Error('Screenshot name cannot be an absolute path');
32
38
  }
33
39
 
34
- // Allow only safe characters: alphanumeric, hyphens, underscores, and dots
40
+ // Allow only safe characters: alphanumeric, hyphens, underscores, dots, and optionally slashes
35
41
  // Replace other characters with underscores
36
- let sanitized = name.replace(/[^a-zA-Z0-9._-]/g, '_');
42
+ let allowedChars = allowSlashes ? /[^a-zA-Z0-9._/-]/g : /[^a-zA-Z0-9._-]/g;
43
+ let sanitized = name.replace(allowedChars, '_');
37
44
 
38
45
  // Prevent names that start with dots (hidden files)
39
46
  if (sanitized.startsWith('.')) {
@@ -116,7 +123,9 @@ export function validateScreenshotProperties(properties = {}) {
116
123
  // Validate common properties with safe constraints
117
124
  if (properties.browser && typeof properties.browser === 'string') {
118
125
  try {
119
- validated.browser = sanitizeScreenshotName(properties.browser, 50);
126
+ // Extract browser name without version (e.g., "Chrome/139.0.7258.138" -> "Chrome")
127
+ let browserName = properties.browser.split('/')[0];
128
+ validated.browser = sanitizeScreenshotName(browserName, 50);
120
129
  } catch (error) {
121
130
  // Skip invalid browser names, don't include them
122
131
  logger.warn(`Invalid browser name '${properties.browser}': ${error.message}`);
@@ -470,7 +470,7 @@ Configuration loaded via cosmiconfig in this order:
470
470
  {
471
471
  // API Configuration
472
472
  apiKey: string, // API token (from VIZZLY_TOKEN)
473
- apiUrl: string, // API base URL (default: 'https://vizzly.dev')
473
+ apiUrl: string, // API base URL (default: 'https://app.vizzly.dev')
474
474
  project: string, // Project ID override
475
475
 
476
476
  // Server Configuration (for run command)
@@ -496,9 +496,7 @@ Configuration loaded via cosmiconfig in this order:
496
496
 
497
497
  // Comparison Configuration
498
498
  comparison: {
499
- threshold: number, // Pixel difference threshold (default: 0.01)
500
- ignoreAntialiasing: boolean, // Ignore antialiasing (default: true)
501
- ignoreColors: boolean // Ignore color differences (default: false)
499
+ threshold: number // Pixel difference threshold (default: 0.1)
502
500
  }
503
501
  }
504
502
  ```
@@ -30,7 +30,7 @@ vizzly doctor --json
30
30
 
31
31
  ## Environment Variables
32
32
 
33
- - `VIZZLY_API_URL` — Override the API base URL (default: `https://vizzly.dev`)
33
+ - `VIZZLY_API_URL` — Override the API base URL (default: `https://app.vizzly.dev`)
34
34
  - `VIZZLY_TOKEN` — API token used only when `--api` is provided
35
35
 
36
36
  ## Exit Codes
@@ -59,7 +59,7 @@ npx vizzly upload ./screenshots --build-name "Release v1.2.3"
59
59
  npx vizzly run "npm test"
60
60
 
61
61
  # Use TDD mode for local development
62
- npx vizzly tdd "npm test"
62
+ npx vizzly tdd run "npm test"
63
63
  ```
64
64
 
65
65
  ### 6. In your test code