@vizzly-testing/cli 0.7.1 → 0.8.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.
package/README.md CHANGED
@@ -368,7 +368,7 @@ Check if Vizzly is enabled in the current environment.
368
368
 
369
369
  ### Core Configuration
370
370
  - `VIZZLY_TOKEN`: API authentication token. Example: `export VIZZLY_TOKEN=your-token`.
371
- - `VIZZLY_API_URL`: Override API base URL. Default: `https://vizzly.dev`.
371
+ - `VIZZLY_API_URL`: Override API base URL. Default: `https://app.vizzly.dev`.
372
372
  - `VIZZLY_LOG_LEVEL`: Logger level. One of `debug`, `info`, `warn`, `error`. Example: `export VIZZLY_LOG_LEVEL=debug`.
373
373
 
374
374
  ### Parallel Builds
@@ -190,7 +190,9 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
190
190
  // Silent fail on cleanup
191
191
  }
192
192
  }
193
- ui.error('Upload failed', error);
193
+ // Use user-friendly error message if available
194
+ const errorMessage = error?.getUserMessage ? error.getUserMessage() : error.message;
195
+ ui.error(errorMessage || 'Upload failed', error);
194
196
  }
195
197
  }
196
198
 
package/dist/sdk/index.js CHANGED
@@ -30,7 +30,7 @@ import { VizzlyError } from '../errors/vizzly-error.js';
30
30
  *
31
31
  * const vizzly = await createVizzly({
32
32
  * apiKey: process.env.VIZZLY_TOKEN,
33
- * apiUrl: 'https://vizzly.dev',
33
+ * apiUrl: 'https://app.vizzly.dev',
34
34
  * server: {
35
35
  * port: 3003,
36
36
  * enabled: true
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { URLSearchParams } from 'url';
7
- import { VizzlyError } from '../errors/vizzly-error.js';
7
+ import { VizzlyError, AuthError } from '../errors/vizzly-error.js';
8
8
  import crypto from 'crypto';
9
9
  import { getPackageVersion } from '../utils/package-info.js';
10
10
  import { getApiUrl, getApiToken, getUserAgent } from '../utils/environment-config.js';
@@ -58,6 +58,11 @@ export class ApiService {
58
58
  } catch {
59
59
  // ignore
60
60
  }
61
+
62
+ // Handle authentication errors with user-friendly messages
63
+ if (response.status === 401) {
64
+ throw new AuthError('Invalid or expired API token. Please check your VIZZLY_TOKEN environment variable and ensure it is valid.');
65
+ }
61
66
  throw new VizzlyError(`API request failed: ${response.status}${errorText ? ` - ${errorText}` : ''} (URL: ${url})`);
62
67
  }
63
68
  return response.json();
@@ -113,29 +118,44 @@ export class ApiService {
113
118
 
114
119
  /**
115
120
  * Check if SHAs already exist on the server
116
- * @param {string[]} shas - Array of SHA256 hashes to check
121
+ * @param {string[]|Object[]} shas - Array of SHA256 hashes to check, or array of screenshot objects with metadata
117
122
  * @param {string} buildId - Build ID for screenshot record creation
118
123
  * @returns {Promise<Object>} Response with existing SHAs and screenshot data
119
124
  */
120
125
  async checkShas(shas, buildId) {
121
126
  try {
127
+ let requestBody;
128
+
129
+ // Check if we're using the new signature-based format (array of objects) or legacy format (array of strings)
130
+ if (Array.isArray(shas) && shas.length > 0 && typeof shas[0] === 'object' && shas[0].sha256) {
131
+ // New signature-based format
132
+ requestBody = {
133
+ buildId,
134
+ screenshots: shas
135
+ };
136
+ } else {
137
+ // Legacy SHA-only format
138
+ requestBody = {
139
+ shas,
140
+ buildId
141
+ };
142
+ }
122
143
  const response = await this.request('/api/sdk/check-shas', {
123
144
  method: 'POST',
124
145
  headers: {
125
146
  'Content-Type': 'application/json'
126
147
  },
127
- body: JSON.stringify({
128
- shas,
129
- buildId
130
- })
148
+ body: JSON.stringify(requestBody)
131
149
  });
132
150
  return response;
133
151
  } catch (error) {
134
152
  // Continue without deduplication on error
135
153
  console.debug('SHA check failed, continuing without deduplication:', error.message);
154
+ // Extract SHAs for fallback response regardless of format
155
+ const shaList = Array.isArray(shas) && shas.length > 0 && typeof shas[0] === 'object' ? shas.map(s => s.sha256) : shas;
136
156
  return {
137
157
  existing: [],
138
- missing: shas,
158
+ missing: shaList,
139
159
  screenshots: []
140
160
  };
141
161
  }
@@ -167,13 +187,22 @@ export class ApiService {
167
187
  });
168
188
  }
169
189
 
170
- // Normal flow with SHA deduplication
190
+ // Normal flow with SHA deduplication using signature-based format
171
191
  const sha256 = crypto.createHash('sha256').update(buffer).digest('hex');
172
192
 
173
- // Check if this SHA already exists
174
- const checkResult = await this.checkShas([sha256], buildId);
193
+ // Create screenshot object with signature data for checking
194
+ const screenshotCheck = [{
195
+ sha256,
196
+ name,
197
+ browser: metadata?.browser || 'chrome',
198
+ viewport_width: metadata?.viewport?.width || 1920,
199
+ viewport_height: metadata?.viewport?.height || 1080
200
+ }];
201
+
202
+ // Check if this SHA with signature already exists
203
+ const checkResult = await this.checkShas(screenshotCheck, buildId);
175
204
  if (checkResult.existing && checkResult.existing.includes(sha256)) {
176
- // File already exists, screenshot record was automatically created
205
+ // File already exists with same signature, screenshot record was automatically created
177
206
  const screenshot = checkResult.screenshots?.find(s => s.sha256 === sha256);
178
207
  return {
179
208
  message: 'Screenshot already exists, skipped upload',
@@ -184,7 +213,7 @@ export class ApiService {
184
213
  };
185
214
  }
186
215
 
187
- // File doesn't exist, proceed with upload
216
+ // File doesn't exist or has different signature, proceed with upload
188
217
  return this.request(`/api/sdk/builds/${buildId}/screenshots`, {
189
218
  method: 'POST',
190
219
  headers: {
@@ -274,29 +274,31 @@ async function processFiles(files, signal, onProgress) {
274
274
  }
275
275
 
276
276
  /**
277
- * Check which files already exist on the server
277
+ * Check which files already exist on the server using signature-based deduplication
278
278
  */
279
279
  async function checkExistingFiles(fileMetadata, api, signal, buildId) {
280
- const allShas = fileMetadata.map(f => f.sha256);
281
280
  const existingShas = new Set();
282
281
  const allScreenshots = [];
283
282
 
284
- // Check in batches
285
- for (let i = 0; i < allShas.length; i += DEFAULT_SHA_CHECK_BATCH_SIZE) {
283
+ // Check in batches using the new signature-based format
284
+ for (let i = 0; i < fileMetadata.length; i += DEFAULT_SHA_CHECK_BATCH_SIZE) {
286
285
  if (signal.aborted) throw new UploadError('Operation cancelled');
287
- const batch = allShas.slice(i, i + DEFAULT_SHA_CHECK_BATCH_SIZE);
286
+ const batch = fileMetadata.slice(i, i + DEFAULT_SHA_CHECK_BATCH_SIZE);
287
+
288
+ // Convert file metadata to screenshot objects with signature data
289
+ const screenshotBatch = batch.map(file => ({
290
+ sha256: file.sha256,
291
+ name: file.filename.replace(/\.png$/, ''),
292
+ // Remove .png extension for name
293
+ // Extract browser from filename if available (e.g., "homepage-chrome.png" -> "chrome")
294
+ browser: extractBrowserFromFilename(file.filename) || 'chrome',
295
+ // Default to chrome
296
+ // Default viewport dimensions (these could be extracted from filename or metadata if available)
297
+ viewport_width: 1920,
298
+ viewport_height: 1080
299
+ }));
288
300
  try {
289
- const res = await api.request('/api/sdk/check-shas', {
290
- method: 'POST',
291
- headers: {
292
- 'Content-Type': 'application/json'
293
- },
294
- body: JSON.stringify({
295
- shas: batch,
296
- buildId
297
- }),
298
- signal
299
- });
301
+ const res = await api.checkShas(screenshotBatch, buildId);
300
302
  const {
301
303
  existing = [],
302
304
  screenshots = []
@@ -315,6 +317,22 @@ async function checkExistingFiles(fileMetadata, api, signal, buildId) {
315
317
  };
316
318
  }
317
319
 
320
+ /**
321
+ * Extract browser name from filename
322
+ * @param {string} filename - The screenshot filename
323
+ * @returns {string|null} Browser name or null if not found
324
+ */
325
+ function extractBrowserFromFilename(filename) {
326
+ const browsers = ['chrome', 'firefox', 'safari', 'edge', 'webkit'];
327
+ const lowerFilename = filename.toLowerCase();
328
+ for (const browser of browsers) {
329
+ if (lowerFilename.includes(browser)) {
330
+ return browser;
331
+ }
332
+ }
333
+ return null;
334
+ }
335
+
318
336
  /**
319
337
  * Upload files to Vizzly
320
338
  */
@@ -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
@@ -41,11 +41,11 @@ export class ApiService {
41
41
  createBuild(metadata: any): Promise<any>;
42
42
  /**
43
43
  * Check if SHAs already exist on the server
44
- * @param {string[]} shas - Array of SHA256 hashes to check
44
+ * @param {string[]|Object[]} shas - Array of SHA256 hashes to check, or array of screenshot objects with metadata
45
45
  * @param {string} buildId - Build ID for screenshot record creation
46
46
  * @returns {Promise<Object>} Response with existing SHAs and screenshot data
47
47
  */
48
- checkShas(shas: string[], buildId: string): Promise<any>;
48
+ checkShas(shas: string[] | any[], buildId: string): Promise<any>;
49
49
  /**
50
50
  * Upload a screenshot with SHA checking
51
51
  * @param {string} buildId - Build ID
@@ -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
  /**
@@ -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)
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Visual review platform for UI developers and designers",
5
5
  "keywords": [
6
6
  "visual-testing",