@vizzly-testing/cli 0.23.1 → 0.24.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 +54 -586
- package/dist/api/client.js +3 -1
- package/dist/api/endpoints.js +6 -7
- package/dist/cli.js +15 -2
- package/dist/commands/finalize.js +12 -0
- package/dist/commands/preview.js +210 -28
- package/dist/commands/run.js +15 -0
- package/dist/commands/status.js +34 -8
- package/dist/commands/upload.js +13 -0
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +71 -63
- package/dist/server/handlers/tdd-handler.js +4 -9
- package/dist/tdd/core/region-coverage.js +115 -0
- package/dist/tdd/core/signature.js +3 -1
- package/dist/tdd/metadata/region-metadata.js +93 -0
- package/dist/tdd/services/comparison-service.js +40 -8
- package/dist/tdd/services/region-service.js +75 -0
- package/dist/tdd/tdd-service.js +70 -8
- package/package.json +3 -4
- package/claude-plugin/.claude-plugin/README.md +0 -270
- package/claude-plugin/.claude-plugin/marketplace.json +0 -28
- package/claude-plugin/.claude-plugin/plugin.json +0 -14
- package/claude-plugin/.mcp.json +0 -12
- package/claude-plugin/CHANGELOG.md +0 -85
- package/claude-plugin/commands/setup.md +0 -137
- package/claude-plugin/commands/suggest-screenshots.md +0 -111
- package/claude-plugin/mcp/vizzly-docs-server/README.md +0 -95
- package/claude-plugin/mcp/vizzly-docs-server/docs-fetcher.js +0 -110
- package/claude-plugin/mcp/vizzly-docs-server/index.js +0 -283
- package/claude-plugin/mcp/vizzly-server/cloud-api-provider.js +0 -399
- package/claude-plugin/mcp/vizzly-server/index.js +0 -927
- package/claude-plugin/mcp/vizzly-server/local-tdd-provider.js +0 -455
- package/claude-plugin/mcp/vizzly-server/token-resolver.js +0 -185
- package/claude-plugin/skills/check-visual-tests/SKILL.md +0 -158
- package/claude-plugin/skills/debug-visual-regression/SKILL.md +0 -269
- package/docs/api-reference.md +0 -1003
- package/docs/authentication.md +0 -334
- package/docs/doctor-command.md +0 -44
- package/docs/getting-started.md +0 -131
- package/docs/internal/SDK-API.md +0 -1018
- package/docs/plugins.md +0 -557
- package/docs/tdd-mode.md +0 -594
- package/docs/test-integration.md +0 -523
- package/docs/tui-elements.md +0 -560
- package/docs/upload-command.md +0 -196
package/dist/api/client.js
CHANGED
|
@@ -78,7 +78,9 @@ export function createApiClient(options = {}) {
|
|
|
78
78
|
|
|
79
79
|
// Other errors
|
|
80
80
|
let error = parseApiError(response.status, errorBody, url);
|
|
81
|
-
throw new VizzlyError(error.message, error.code
|
|
81
|
+
throw new VizzlyError(error.message, error.code, {
|
|
82
|
+
status: error.status
|
|
83
|
+
});
|
|
82
84
|
}
|
|
83
85
|
return response.json();
|
|
84
86
|
}
|
package/dist/api/endpoints.js
CHANGED
|
@@ -325,17 +325,16 @@ export async function finalizeParallelBuild(client, parallelId) {
|
|
|
325
325
|
* @returns {Promise<Object>} Upload result with preview URL
|
|
326
326
|
*/
|
|
327
327
|
export async function uploadPreviewZip(client, buildId, zipBuffer) {
|
|
328
|
-
//
|
|
329
|
-
let FormData = (await import('form-data')).default;
|
|
328
|
+
// Use native FormData (Node 18+) with Blob for proper fetch compatibility
|
|
330
329
|
let formData = new FormData();
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
contentType: 'application/zip'
|
|
330
|
+
let blob = new Blob([zipBuffer], {
|
|
331
|
+
type: 'application/zip'
|
|
334
332
|
});
|
|
333
|
+
formData.append('file', blob, 'preview.zip');
|
|
335
334
|
return client.request(`/api/sdk/builds/${buildId}/preview/upload-zip`, {
|
|
336
335
|
method: 'POST',
|
|
337
|
-
body: formData
|
|
338
|
-
|
|
336
|
+
body: formData
|
|
337
|
+
// Let fetch set the Content-Type with boundary automatically
|
|
339
338
|
});
|
|
340
339
|
}
|
|
341
340
|
|
package/dist/cli.js
CHANGED
|
@@ -63,7 +63,7 @@ const formatHelp = (cmd, helper) => {
|
|
|
63
63
|
key: 'core',
|
|
64
64
|
icon: '▸',
|
|
65
65
|
title: 'Core',
|
|
66
|
-
names: ['run', 'tdd', 'upload', 'status', 'finalize']
|
|
66
|
+
names: ['run', 'tdd', 'upload', 'status', 'finalize', 'preview']
|
|
67
67
|
}, {
|
|
68
68
|
key: 'setup',
|
|
69
69
|
icon: '▸',
|
|
@@ -412,9 +412,22 @@ program.command('finalize').description('Finalize a parallel build after all sha
|
|
|
412
412
|
}
|
|
413
413
|
await finalizeCommand(parallelId, options, globalOptions);
|
|
414
414
|
});
|
|
415
|
-
program.command('preview').description('Upload static files as a preview for a build').argument('
|
|
415
|
+
program.command('preview').description('Upload static files as a preview for a build').argument('[path]', 'Path to static files (dist/, build/, out/)').option('-b, --build <id>', 'Build ID to attach preview to').option('-p, --parallel-id <id>', 'Look up build by parallel ID').option('--base <path>', 'Override auto-detected base path').option('--open', 'Open preview URL in browser after upload').option('--dry-run', 'Show what would be uploaded without uploading').option('-x, --exclude <pattern>', 'Exclude files/dirs (repeatable, e.g. -x "*.log" -x "temp/")', (val, prev) => prev ? [...prev, val] : [val]).option('-i, --include <pattern>', 'Override default exclusions (repeatable, e.g. -i package.json -i tests/)', (val, prev) => prev ? [...prev, val] : [val]).action(async (path, options) => {
|
|
416
416
|
const globalOptions = program.opts();
|
|
417
417
|
|
|
418
|
+
// Show helpful error if path is missing
|
|
419
|
+
if (!path) {
|
|
420
|
+
output.error('Path to static files is required');
|
|
421
|
+
output.blank();
|
|
422
|
+
output.print(' Upload your build output directory:');
|
|
423
|
+
output.blank();
|
|
424
|
+
output.print(' vizzly preview ./dist');
|
|
425
|
+
output.print(' vizzly preview ./build');
|
|
426
|
+
output.print(' vizzly preview ./out');
|
|
427
|
+
output.blank();
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
|
|
418
431
|
// Validate options
|
|
419
432
|
const validationErrors = validatePreviewOptions(path, options);
|
|
420
433
|
if (validationErrors.length > 0) {
|
|
@@ -89,6 +89,18 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
|
|
|
89
89
|
};
|
|
90
90
|
} catch (error) {
|
|
91
91
|
output.stopSpinner();
|
|
92
|
+
|
|
93
|
+
// Don't fail CI for Vizzly infrastructure issues (5xx errors)
|
|
94
|
+
let status = error.context?.status;
|
|
95
|
+
if (status >= 500) {
|
|
96
|
+
output.warn('Vizzly API unavailable - finalize skipped.');
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
result: {
|
|
100
|
+
skipped: true
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
92
104
|
output.error('Failed to finalize parallel build', error);
|
|
93
105
|
exit(1);
|
|
94
106
|
return {
|
package/dist/commands/preview.js
CHANGED
|
@@ -20,6 +20,9 @@ import * as defaultOutput from '../utils/output.js';
|
|
|
20
20
|
import { formatSessionAge as defaultFormatSessionAge, readSession as defaultReadSession } from '../utils/session.js';
|
|
21
21
|
let execAsync = promisify(exec);
|
|
22
22
|
|
|
23
|
+
// Maximum files to show in dry-run output (use --verbose for all)
|
|
24
|
+
let DRY_RUN_FILE_LIMIT = 50;
|
|
25
|
+
|
|
23
26
|
/**
|
|
24
27
|
* Validate path for shell safety - prevents command injection
|
|
25
28
|
* @param {string} path - Path to validate
|
|
@@ -74,17 +77,49 @@ function getZipCommand() {
|
|
|
74
77
|
};
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
// Default directories to exclude from preview uploads
|
|
81
|
+
let DEFAULT_EXCLUDED_DIRS = ['node_modules', '__pycache__', '.git', '.svn', '.hg', '.vizzly', 'coverage', '.nyc_output', '.cache', '.turbo', '.next/cache', '.nuxt', '.output', '.vercel', '.netlify', 'tests', 'test', '__tests__', 'spec', '__mocks__', 'playwright-report', 'cypress', '.playwright'];
|
|
82
|
+
|
|
83
|
+
// Default file patterns to exclude from preview uploads
|
|
84
|
+
let DEFAULT_EXCLUDED_FILES = ['package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb', '*.config.js', '*.config.ts', '*.config.mjs', '*.config.cjs', 'tsconfig.json', 'jsconfig.json', '.eslintrc*', '.prettierrc*', 'Makefile', 'Dockerfile', 'docker-compose*.yml', '*.md', '*.log', '*.map'];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if a filename matches any of the exclusion patterns
|
|
88
|
+
* @param {string} filename - Filename to check
|
|
89
|
+
* @param {string[]} patterns - Patterns to match against
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
function matchesPattern(filename, patterns) {
|
|
93
|
+
for (let pattern of patterns) {
|
|
94
|
+
if (pattern.includes('*')) {
|
|
95
|
+
// Simple glob matching - convert to regex
|
|
96
|
+
let regex = new RegExp(`^${pattern.replace(/\./g, '\\.').replace(/\*/g, '.*')}$`);
|
|
97
|
+
if (regex.test(filename)) return true;
|
|
98
|
+
} else {
|
|
99
|
+
if (filename === pattern) return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
77
105
|
/**
|
|
78
106
|
* Create a ZIP file from a directory using system commands
|
|
79
107
|
* @param {string} sourceDir - Directory to zip
|
|
80
108
|
* @param {string} outputPath - Path for output ZIP file
|
|
109
|
+
* @param {Object} exclusions - Exclusion patterns
|
|
110
|
+
* @param {string[]} exclusions.dirs - Directory names to exclude
|
|
111
|
+
* @param {string[]} exclusions.files - File patterns to exclude
|
|
81
112
|
* @returns {Promise<void>}
|
|
82
113
|
*/
|
|
83
|
-
async function createZipWithSystem(sourceDir, outputPath) {
|
|
114
|
+
async function createZipWithSystem(sourceDir, outputPath, exclusions = {}) {
|
|
84
115
|
let {
|
|
85
116
|
command,
|
|
86
117
|
available
|
|
87
118
|
} = getZipCommand();
|
|
119
|
+
let {
|
|
120
|
+
dirs = [],
|
|
121
|
+
files = []
|
|
122
|
+
} = exclusions;
|
|
88
123
|
if (!available) {
|
|
89
124
|
throw new Error('No zip command found. Please install zip or use PowerShell on Windows.');
|
|
90
125
|
}
|
|
@@ -95,22 +130,44 @@ async function createZipWithSystem(sourceDir, outputPath) {
|
|
|
95
130
|
if (!isPathSafe(sourceDir)) {
|
|
96
131
|
throw new Error('Path contains unsupported characters. Please use a path without special shell characters.');
|
|
97
132
|
}
|
|
133
|
+
|
|
134
|
+
// Validate exclusion patterns to prevent command injection
|
|
135
|
+
// Only allow safe characters in patterns: alphanumeric, dots, asterisks, underscores, hyphens, slashes
|
|
136
|
+
let safePatternRegex = /^[a-zA-Z0-9.*_\-/]+$/;
|
|
137
|
+
for (let pattern of [...dirs, ...files]) {
|
|
138
|
+
if (!safePatternRegex.test(pattern)) {
|
|
139
|
+
throw new Error(`Exclusion pattern contains unsafe characters: ${pattern}. Only alphanumeric, ., *, _, -, / are allowed.`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
98
142
|
if (command === 'zip') {
|
|
99
143
|
// Standard zip command - create ZIP from directory contents
|
|
100
144
|
// Using cwd option is safe as it's not part of the command string
|
|
101
|
-
// -r: recursive, -q: quiet
|
|
102
|
-
|
|
145
|
+
// -r: recursive, -q: quiet, -x: exclude patterns
|
|
146
|
+
let excludeArgs = [...dirs.map(dir => `-x "${dir}/*"`), ...files.map(pattern => `-x "${pattern}"`)].join(' ');
|
|
147
|
+
await execAsync(`zip -r -q "${outputPath}" . ${excludeArgs}`, {
|
|
103
148
|
cwd: sourceDir,
|
|
104
149
|
maxBuffer: 1024 * 1024 * 100 // 100MB buffer
|
|
105
150
|
});
|
|
106
151
|
} else if (command === 'powershell') {
|
|
107
|
-
// Windows PowerShell -
|
|
108
|
-
//
|
|
152
|
+
// Windows PowerShell - Compress-Archive doesn't support exclusions,
|
|
153
|
+
// so we create a temp directory with only the files we want
|
|
109
154
|
let safeSrcDir = sourceDir.replace(/'/g, "''");
|
|
110
155
|
let safeOutPath = outputPath.replace(/'/g, "''");
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
156
|
+
|
|
157
|
+
// Build exclusion filter for PowerShell
|
|
158
|
+
// We use Get-ChildItem with -Exclude and pipe to Compress-Archive
|
|
159
|
+
let excludePatterns = [...dirs, ...files].map(p => `'${p}'`).join(',');
|
|
160
|
+
if (excludePatterns) {
|
|
161
|
+
// Use robocopy to copy files excluding patterns, then zip
|
|
162
|
+
// This is more reliable than PowerShell's native filtering
|
|
163
|
+
await execAsync(`powershell -Command "` + `$src = '${safeSrcDir}'; ` + `$dst = '${safeOutPath}'; ` + `$exclude = @(${excludePatterns}); ` + `$items = Get-ChildItem -Path $src -Recurse -File | Where-Object { ` + `$rel = $_.FullName.Substring($src.Length + 1); ` + `$dominated = $false; ` + `foreach ($ex in $exclude) { if ($rel -like $ex -or $rel -like \\"$ex/*\\" -or $_.Name -like $ex) { $dominated = $true; break } }; ` + `-not $dominated ` + `}; ` + `if ($items) { $items | Compress-Archive -DestinationPath $dst -Force }"`, {
|
|
164
|
+
maxBuffer: 1024 * 1024 * 100
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
await execAsync(`powershell -Command "Compress-Archive -LiteralPath '${safeSrcDir}\\*' -DestinationPath '${safeOutPath}' -Force"`, {
|
|
168
|
+
maxBuffer: 1024 * 1024 * 100
|
|
169
|
+
});
|
|
170
|
+
}
|
|
114
171
|
}
|
|
115
172
|
}
|
|
116
173
|
|
|
@@ -118,14 +175,24 @@ async function createZipWithSystem(sourceDir, outputPath) {
|
|
|
118
175
|
* Count files in a directory recursively
|
|
119
176
|
* Skips symlinks to prevent path traversal attacks
|
|
120
177
|
* @param {string} dir - Directory path
|
|
121
|
-
* @
|
|
178
|
+
* @param {Object} options - Options
|
|
179
|
+
* @param {boolean} options.collectPaths - Whether to collect file paths
|
|
180
|
+
* @param {string[]} options.excludedDirs - Directory names to exclude
|
|
181
|
+
* @param {string[]} options.excludedFiles - File patterns to exclude
|
|
182
|
+
* @returns {Promise<{ count: number, totalSize: number, files?: Array<{path: string, size: number}> }>}
|
|
122
183
|
*/
|
|
123
|
-
async function countFiles(dir) {
|
|
184
|
+
async function countFiles(dir, options = {}) {
|
|
124
185
|
let {
|
|
125
186
|
readdir
|
|
126
187
|
} = await import('node:fs/promises');
|
|
188
|
+
let {
|
|
189
|
+
collectPaths = false,
|
|
190
|
+
excludedDirs = DEFAULT_EXCLUDED_DIRS,
|
|
191
|
+
excludedFiles = DEFAULT_EXCLUDED_FILES
|
|
192
|
+
} = options;
|
|
127
193
|
let count = 0;
|
|
128
194
|
let totalSize = 0;
|
|
195
|
+
let files = collectPaths ? [] : null;
|
|
129
196
|
|
|
130
197
|
// Resolve the base directory to an absolute path for traversal checks
|
|
131
198
|
let baseDir = await realpath(resolve(dir));
|
|
@@ -141,8 +208,8 @@ async function countFiles(dir) {
|
|
|
141
208
|
continue;
|
|
142
209
|
}
|
|
143
210
|
if (entry.isDirectory()) {
|
|
144
|
-
// Skip hidden directories and
|
|
145
|
-
if (entry.name.startsWith('.') ||
|
|
211
|
+
// Skip hidden directories and excluded directories
|
|
212
|
+
if (entry.name.startsWith('.') || excludedDirs.includes(entry.name)) {
|
|
146
213
|
continue;
|
|
147
214
|
}
|
|
148
215
|
|
|
@@ -157,16 +224,30 @@ async function countFiles(dir) {
|
|
|
157
224
|
if (entry.name.startsWith('.')) {
|
|
158
225
|
continue;
|
|
159
226
|
}
|
|
227
|
+
|
|
228
|
+
// Skip excluded file patterns
|
|
229
|
+
if (matchesPattern(entry.name, excludedFiles)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
160
232
|
count++;
|
|
161
233
|
let fileStat = await stat(fullPath);
|
|
162
234
|
totalSize += fileStat.size;
|
|
235
|
+
if (files) {
|
|
236
|
+
// Store relative path from base directory
|
|
237
|
+
let relativePath = fullPath.slice(baseDir.length + 1);
|
|
238
|
+
files.push({
|
|
239
|
+
path: relativePath,
|
|
240
|
+
size: fileStat.size
|
|
241
|
+
});
|
|
242
|
+
}
|
|
163
243
|
}
|
|
164
244
|
}
|
|
165
245
|
}
|
|
166
246
|
await walk(baseDir);
|
|
167
247
|
return {
|
|
168
248
|
count,
|
|
169
|
-
totalSize
|
|
249
|
+
totalSize,
|
|
250
|
+
files
|
|
170
251
|
};
|
|
171
252
|
}
|
|
172
253
|
|
|
@@ -213,8 +294,8 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
213
294
|
};
|
|
214
295
|
let config = await loadConfig(globalOptions.config, allOptions);
|
|
215
296
|
|
|
216
|
-
// Validate API token
|
|
217
|
-
if (!config.apiKey) {
|
|
297
|
+
// Validate API token (skip for dry-run)
|
|
298
|
+
if (!options.dryRun && !config.apiKey) {
|
|
218
299
|
output.error('API token required. Use --token or set VIZZLY_TOKEN environment variable');
|
|
219
300
|
exit(1);
|
|
220
301
|
return {
|
|
@@ -296,23 +377,64 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
296
377
|
};
|
|
297
378
|
}
|
|
298
379
|
|
|
299
|
-
// Check for zip command availability
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
380
|
+
// Check for zip command availability (skip for dry-run)
|
|
381
|
+
if (!options.dryRun) {
|
|
382
|
+
let zipInfo = getZipCommand();
|
|
383
|
+
if (!zipInfo.available) {
|
|
384
|
+
output.error('No zip command found. Please install zip (macOS/Linux) or ensure PowerShell is available (Windows).');
|
|
385
|
+
exit(1);
|
|
386
|
+
return {
|
|
387
|
+
success: false,
|
|
388
|
+
reason: 'no-zip-command'
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Build exclusion lists from defaults and user options
|
|
394
|
+
let excludedDirs = [...DEFAULT_EXCLUDED_DIRS];
|
|
395
|
+
let excludedFiles = [...DEFAULT_EXCLUDED_FILES];
|
|
396
|
+
|
|
397
|
+
// Add user-specified exclusions
|
|
398
|
+
if (options.exclude) {
|
|
399
|
+
let userExcludes = Array.isArray(options.exclude) ? options.exclude : [options.exclude];
|
|
400
|
+
for (let pattern of userExcludes) {
|
|
401
|
+
// If pattern ends with /, treat as directory
|
|
402
|
+
if (pattern.endsWith('/')) {
|
|
403
|
+
excludedDirs.push(pattern.slice(0, -1));
|
|
404
|
+
} else {
|
|
405
|
+
excludedFiles.push(pattern);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
308
408
|
}
|
|
309
409
|
|
|
410
|
+
// Remove patterns that user explicitly wants to include
|
|
411
|
+
if (options.include) {
|
|
412
|
+
let userIncludes = Array.isArray(options.include) ? options.include : [options.include];
|
|
413
|
+
for (let pattern of userIncludes) {
|
|
414
|
+
if (pattern.endsWith('/')) {
|
|
415
|
+
let dirName = pattern.slice(0, -1);
|
|
416
|
+
excludedDirs = excludedDirs.filter(d => d !== dirName);
|
|
417
|
+
} else {
|
|
418
|
+
excludedFiles = excludedFiles.filter(f => f !== pattern);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
let exclusions = {
|
|
423
|
+
dirs: excludedDirs,
|
|
424
|
+
files: excludedFiles
|
|
425
|
+
};
|
|
426
|
+
|
|
310
427
|
// Count files and calculate size
|
|
311
428
|
output.startSpinner('Scanning files...');
|
|
312
429
|
let {
|
|
313
430
|
count: fileCount,
|
|
314
|
-
totalSize
|
|
315
|
-
|
|
431
|
+
totalSize,
|
|
432
|
+
files
|
|
433
|
+
} = await countFiles(path, {
|
|
434
|
+
collectPaths: options.dryRun,
|
|
435
|
+
excludedDirs,
|
|
436
|
+
excludedFiles
|
|
437
|
+
});
|
|
316
438
|
if (fileCount === 0) {
|
|
317
439
|
output.stopSpinner();
|
|
318
440
|
output.error(`No files found in ${path}`);
|
|
@@ -322,7 +444,67 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
322
444
|
reason: 'no-files'
|
|
323
445
|
};
|
|
324
446
|
}
|
|
325
|
-
output.
|
|
447
|
+
output.stopSpinner();
|
|
448
|
+
|
|
449
|
+
// Dry run - show what would be uploaded and exit
|
|
450
|
+
if (options.dryRun) {
|
|
451
|
+
let colors = output.getColors();
|
|
452
|
+
if (globalOptions.json) {
|
|
453
|
+
output.data({
|
|
454
|
+
dryRun: true,
|
|
455
|
+
path: resolve(path),
|
|
456
|
+
fileCount,
|
|
457
|
+
totalSize,
|
|
458
|
+
excludedDirs,
|
|
459
|
+
excludedFiles,
|
|
460
|
+
files: files.map(f => ({
|
|
461
|
+
path: f.path,
|
|
462
|
+
size: f.size
|
|
463
|
+
}))
|
|
464
|
+
});
|
|
465
|
+
} else {
|
|
466
|
+
output.info(`Dry run - would upload ${fileCount} files (${formatBytes(totalSize)})`);
|
|
467
|
+
output.blank();
|
|
468
|
+
output.print(` ${colors.brand.textTertiary('Source')} ${resolve(path)}`);
|
|
469
|
+
output.print(` ${colors.brand.textTertiary('Files')} ${fileCount}`);
|
|
470
|
+
output.print(` ${colors.brand.textTertiary('Total size')} ${formatBytes(totalSize)}`);
|
|
471
|
+
output.blank();
|
|
472
|
+
|
|
473
|
+
// Show exclusions in verbose mode
|
|
474
|
+
if (globalOptions.verbose) {
|
|
475
|
+
output.print(` ${colors.brand.textTertiary('Excluded directories:')}`);
|
|
476
|
+
for (let dir of excludedDirs) {
|
|
477
|
+
output.print(` ${colors.dim(dir)}`);
|
|
478
|
+
}
|
|
479
|
+
output.blank();
|
|
480
|
+
output.print(` ${colors.brand.textTertiary('Excluded file patterns:')}`);
|
|
481
|
+
for (let pattern of excludedFiles) {
|
|
482
|
+
output.print(` ${colors.dim(pattern)}`);
|
|
483
|
+
}
|
|
484
|
+
output.blank();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Show files (limit in non-verbose mode)
|
|
488
|
+
let displayFiles = globalOptions.verbose ? files : files.slice(0, DRY_RUN_FILE_LIMIT);
|
|
489
|
+
let hasMore = files.length > displayFiles.length;
|
|
490
|
+
output.print(` ${colors.brand.textTertiary('Files to upload:')}`);
|
|
491
|
+
for (let file of displayFiles) {
|
|
492
|
+
output.print(` ${file.path} ${colors.dim(`(${formatBytes(file.size)})`)}`);
|
|
493
|
+
}
|
|
494
|
+
if (hasMore) {
|
|
495
|
+
output.print(` ${colors.dim(`... and ${files.length - DRY_RUN_FILE_LIMIT} more (use --verbose to see all)`)}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
output.cleanup();
|
|
499
|
+
return {
|
|
500
|
+
success: true,
|
|
501
|
+
dryRun: true,
|
|
502
|
+
fileCount,
|
|
503
|
+
totalSize,
|
|
504
|
+
files
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
output.startSpinner(`Found ${fileCount} files (${formatBytes(totalSize)})`);
|
|
326
508
|
|
|
327
509
|
// Create ZIP using system command
|
|
328
510
|
output.updateSpinner('Compressing files...');
|
|
@@ -331,7 +513,7 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
331
513
|
let zipPath = join(tmpdir(), `vizzly-preview-${Date.now()}-${randomSuffix}.zip`);
|
|
332
514
|
let zipBuffer;
|
|
333
515
|
try {
|
|
334
|
-
await createZipWithSystem(path, zipPath);
|
|
516
|
+
await createZipWithSystem(path, zipPath, exclusions);
|
|
335
517
|
zipBuffer = await readFile(zipPath);
|
|
336
518
|
} catch (zipError) {
|
|
337
519
|
output.stopSpinner();
|
package/dist/commands/run.js
CHANGED
|
@@ -356,6 +356,21 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
356
356
|
} catch (error) {
|
|
357
357
|
output.stopSpinner();
|
|
358
358
|
|
|
359
|
+
// Don't fail CI for Vizzly infrastructure issues (5xx errors)
|
|
360
|
+
let status = error.context?.status;
|
|
361
|
+
if (status >= 500) {
|
|
362
|
+
output.warn('Vizzly API unavailable - visual tests skipped. Your tests still ran.');
|
|
363
|
+
output.debug('api', 'API error details:', {
|
|
364
|
+
error: error.message
|
|
365
|
+
});
|
|
366
|
+
return {
|
|
367
|
+
success: true,
|
|
368
|
+
result: {
|
|
369
|
+
skipped: true
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
359
374
|
// Provide more context about where the error occurred
|
|
360
375
|
let errorContext = 'Test run failed';
|
|
361
376
|
if (error.message?.includes('build')) {
|
package/dist/commands/status.js
CHANGED
|
@@ -3,18 +3,28 @@
|
|
|
3
3
|
* Uses functional API operations directly
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createApiClient, getBuild, getPreviewInfo } from '../api/index.js';
|
|
7
|
-
import { loadConfig } from '../utils/config-loader.js';
|
|
8
|
-
import { getApiUrl } from '../utils/environment-config.js';
|
|
9
|
-
import * as
|
|
6
|
+
import { createApiClient as defaultCreateApiClient, getBuild as defaultGetBuild, getPreviewInfo as defaultGetPreviewInfo } from '../api/index.js';
|
|
7
|
+
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
8
|
+
import { getApiUrl as defaultGetApiUrl } from '../utils/environment-config.js';
|
|
9
|
+
import * as defaultOutput from '../utils/output.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Status command implementation
|
|
13
13
|
* @param {string} buildId - Build ID to check status for
|
|
14
14
|
* @param {Object} options - Command options
|
|
15
15
|
* @param {Object} globalOptions - Global CLI options
|
|
16
|
+
* @param {Object} deps - Dependencies for testing
|
|
16
17
|
*/
|
|
17
|
-
export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
18
|
+
export async function statusCommand(buildId, options = {}, globalOptions = {}, deps = {}) {
|
|
19
|
+
let {
|
|
20
|
+
loadConfig = defaultLoadConfig,
|
|
21
|
+
createApiClient = defaultCreateApiClient,
|
|
22
|
+
getBuild = defaultGetBuild,
|
|
23
|
+
getPreviewInfo = defaultGetPreviewInfo,
|
|
24
|
+
getApiUrl = defaultGetApiUrl,
|
|
25
|
+
output = defaultOutput,
|
|
26
|
+
exit = code => process.exit(code)
|
|
27
|
+
} = deps;
|
|
18
28
|
output.configure({
|
|
19
29
|
json: globalOptions.json,
|
|
20
30
|
verbose: globalOptions.verbose,
|
|
@@ -31,7 +41,7 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
31
41
|
// Validate API token
|
|
32
42
|
if (!config.apiKey) {
|
|
33
43
|
output.error('API token required. Use --token or set VIZZLY_TOKEN environment variable');
|
|
34
|
-
|
|
44
|
+
exit(1);
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
// Get build details via functional API
|
|
@@ -194,11 +204,27 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
194
204
|
|
|
195
205
|
// Exit with appropriate code based on build status
|
|
196
206
|
if (build.status === 'failed' || build.failed_jobs > 0) {
|
|
197
|
-
|
|
207
|
+
exit(1);
|
|
198
208
|
}
|
|
199
209
|
} catch (error) {
|
|
210
|
+
// Don't fail CI for Vizzly infrastructure issues (5xx errors)
|
|
211
|
+
let status = error.context?.status;
|
|
212
|
+
if (status >= 500) {
|
|
213
|
+
output.warn('Vizzly API unavailable - status check skipped.');
|
|
214
|
+
output.cleanup();
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
result: {
|
|
218
|
+
skipped: true
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
200
222
|
output.error('Failed to get build status', error);
|
|
201
|
-
|
|
223
|
+
exit(1);
|
|
224
|
+
return {
|
|
225
|
+
success: false,
|
|
226
|
+
error
|
|
227
|
+
};
|
|
202
228
|
}
|
|
203
229
|
}
|
|
204
230
|
|
package/dist/commands/upload.js
CHANGED
|
@@ -227,6 +227,19 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
227
227
|
result
|
|
228
228
|
};
|
|
229
229
|
} catch (error) {
|
|
230
|
+
// Don't fail CI for Vizzly infrastructure issues (5xx errors)
|
|
231
|
+
let status = error.context?.status;
|
|
232
|
+
if (status >= 500) {
|
|
233
|
+
output.warn('Vizzly API unavailable - upload skipped. Your tests still ran.');
|
|
234
|
+
output.cleanup();
|
|
235
|
+
return {
|
|
236
|
+
success: true,
|
|
237
|
+
result: {
|
|
238
|
+
skipped: true
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
230
243
|
// Mark build as failed if we have a buildId and config
|
|
231
244
|
if (buildId && config) {
|
|
232
245
|
try {
|