@vizzly-testing/cli 0.20.0 → 0.20.1-beta.1
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/dist/api/client.js +134 -0
- package/dist/api/core.js +341 -0
- package/dist/api/endpoints.js +314 -0
- package/dist/api/index.js +19 -0
- package/dist/auth/client.js +91 -0
- package/dist/auth/core.js +176 -0
- package/dist/auth/index.js +30 -0
- package/dist/auth/operations.js +148 -0
- package/dist/cli.js +178 -3
- package/dist/client/index.js +144 -77
- package/dist/commands/doctor.js +121 -36
- package/dist/commands/finalize.js +49 -18
- package/dist/commands/init.js +13 -18
- package/dist/commands/login.js +49 -55
- package/dist/commands/logout.js +17 -9
- package/dist/commands/project.js +100 -71
- package/dist/commands/run.js +189 -95
- package/dist/commands/status.js +101 -66
- package/dist/commands/tdd-daemon.js +61 -32
- package/dist/commands/tdd.js +104 -98
- package/dist/commands/upload.js +78 -34
- package/dist/commands/whoami.js +44 -42
- package/dist/config/core.js +438 -0
- package/dist/config/index.js +13 -0
- package/dist/config/operations.js +327 -0
- package/dist/index.js +1 -1
- package/dist/project/core.js +295 -0
- package/dist/project/index.js +13 -0
- package/dist/project/operations.js +393 -0
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +16 -16
- package/dist/screenshot-server/core.js +157 -0
- package/dist/screenshot-server/index.js +11 -0
- package/dist/screenshot-server/operations.js +183 -0
- package/dist/sdk/index.js +3 -2
- package/dist/server/handlers/api-handler.js +14 -5
- package/dist/server/handlers/tdd-handler.js +191 -53
- package/dist/server/http-server.js +9 -3
- package/dist/server/routers/baseline.js +58 -0
- package/dist/server/routers/dashboard.js +10 -6
- package/dist/server/routers/screenshot.js +32 -0
- package/dist/server-manager/core.js +186 -0
- package/dist/server-manager/index.js +81 -0
- package/dist/server-manager/operations.js +209 -0
- package/dist/services/build-manager.js +2 -69
- package/dist/services/index.js +21 -48
- package/dist/services/screenshot-server.js +40 -74
- package/dist/services/server-manager.js +45 -80
- package/dist/services/test-runner.js +90 -250
- package/dist/services/uploader.js +56 -358
- package/dist/tdd/core/hotspot-coverage.js +112 -0
- package/dist/tdd/core/signature.js +101 -0
- package/dist/tdd/index.js +19 -0
- package/dist/tdd/metadata/baseline-metadata.js +103 -0
- package/dist/tdd/metadata/hotspot-metadata.js +93 -0
- package/dist/tdd/services/baseline-downloader.js +151 -0
- package/dist/tdd/services/baseline-manager.js +166 -0
- package/dist/tdd/services/comparison-service.js +230 -0
- package/dist/tdd/services/hotspot-service.js +71 -0
- package/dist/tdd/services/result-service.js +123 -0
- package/dist/tdd/tdd-service.js +1145 -0
- package/dist/test-runner/core.js +255 -0
- package/dist/test-runner/index.js +13 -0
- package/dist/test-runner/operations.js +483 -0
- package/dist/types/client.d.ts +25 -2
- package/dist/uploader/core.js +396 -0
- package/dist/uploader/index.js +11 -0
- package/dist/uploader/operations.js +412 -0
- package/dist/utils/colors.js +187 -39
- package/dist/utils/config-loader.js +3 -6
- package/dist/utils/context.js +228 -0
- package/dist/utils/output.js +449 -14
- package/docs/api-reference.md +173 -8
- package/docs/tui-elements.md +560 -0
- package/package.json +13 -13
- package/dist/services/api-service.js +0 -412
- package/dist/services/auth-service.js +0 -226
- package/dist/services/config-service.js +0 -369
- package/dist/services/html-report-generator.js +0 -455
- package/dist/services/project-service.js +0 -326
- package/dist/services/report-generator/report.css +0 -411
- package/dist/services/report-generator/viewer.js +0 -102
- package/dist/services/static-report-generator.js +0 -207
- package/dist/services/tdd-service.js +0 -1437
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uploader Operations - I/O operations with dependency injection
|
|
3
|
+
*
|
|
4
|
+
* Each operation takes its dependencies as parameters for testability.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { buildBuildInfo, buildCompletedProgress, buildDeduplicationProgress, buildFileMetadata, buildProcessingProgress, buildScanningProgress, buildScreenshotPattern, buildUploadingProgress, buildUploadResult, buildWaitResult, DEFAULT_SHA_CHECK_BATCH_SIZE, extractStatusCodeFromError, fileToScreenshotFormat, getElapsedTime, isTimedOut, partitionFilesByExistence, validateApiKey, validateDirectoryStats, validateFilesFound, validateScreenshotsDir } from './core.js';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// File Discovery
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find all PNG screenshots in a directory
|
|
15
|
+
* @param {Object} options - Options
|
|
16
|
+
* @param {string} options.directory - Directory to search
|
|
17
|
+
* @param {Object} options.deps - Dependencies
|
|
18
|
+
* @param {Function} options.deps.glob - Glob function
|
|
19
|
+
* @returns {Promise<Array<string>>} Array of file paths
|
|
20
|
+
*/
|
|
21
|
+
export async function findScreenshots({
|
|
22
|
+
directory,
|
|
23
|
+
deps
|
|
24
|
+
}) {
|
|
25
|
+
let {
|
|
26
|
+
glob
|
|
27
|
+
} = deps;
|
|
28
|
+
let pattern = buildScreenshotPattern(directory);
|
|
29
|
+
return glob(pattern, {
|
|
30
|
+
absolute: true
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// File Processing
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Process files to extract metadata and compute hashes
|
|
40
|
+
* @param {Object} options - Options
|
|
41
|
+
* @param {Array<string>} options.files - File paths
|
|
42
|
+
* @param {AbortSignal} options.signal - Abort signal
|
|
43
|
+
* @param {Function} options.onProgress - Progress callback
|
|
44
|
+
* @param {Object} options.deps - Dependencies
|
|
45
|
+
* @param {Function} options.deps.readFile - File read function
|
|
46
|
+
* @param {Function} options.deps.createError - Error factory
|
|
47
|
+
* @returns {Promise<Array>} File metadata array
|
|
48
|
+
*/
|
|
49
|
+
export async function processFiles({
|
|
50
|
+
files,
|
|
51
|
+
signal,
|
|
52
|
+
onProgress,
|
|
53
|
+
deps
|
|
54
|
+
}) {
|
|
55
|
+
let {
|
|
56
|
+
readFile,
|
|
57
|
+
createError
|
|
58
|
+
} = deps;
|
|
59
|
+
let results = [];
|
|
60
|
+
let count = 0;
|
|
61
|
+
for (let filePath of files) {
|
|
62
|
+
if (signal.aborted) {
|
|
63
|
+
throw createError('Operation cancelled', 'UPLOAD_CANCELLED');
|
|
64
|
+
}
|
|
65
|
+
let buffer = await readFile(filePath);
|
|
66
|
+
let metadata = buildFileMetadata(filePath, buffer);
|
|
67
|
+
results.push(metadata);
|
|
68
|
+
count++;
|
|
69
|
+
if (count % 10 === 0 || count === files.length) {
|
|
70
|
+
onProgress(count);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// SHA Checking / Deduplication
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check which files already exist on the server
|
|
82
|
+
* @param {Object} options - Options
|
|
83
|
+
* @param {Array} options.fileMetadata - File metadata array
|
|
84
|
+
* @param {Object} options.client - API client
|
|
85
|
+
* @param {AbortSignal} options.signal - Abort signal
|
|
86
|
+
* @param {string} options.buildId - Build ID
|
|
87
|
+
* @param {Object} options.deps - Dependencies
|
|
88
|
+
* @param {Function} options.deps.checkShas - SHA check API function
|
|
89
|
+
* @param {Function} options.deps.createError - Error factory
|
|
90
|
+
* @param {Object} options.deps.output - Output utilities
|
|
91
|
+
* @returns {Promise<{ toUpload: Array, existing: Array, screenshots: Array }>}
|
|
92
|
+
*/
|
|
93
|
+
export async function checkExistingFiles({
|
|
94
|
+
fileMetadata,
|
|
95
|
+
client,
|
|
96
|
+
signal,
|
|
97
|
+
buildId,
|
|
98
|
+
deps
|
|
99
|
+
}) {
|
|
100
|
+
let {
|
|
101
|
+
checkShas,
|
|
102
|
+
createError,
|
|
103
|
+
output
|
|
104
|
+
} = deps;
|
|
105
|
+
let existingShas = new Set();
|
|
106
|
+
let allScreenshots = [];
|
|
107
|
+
for (let i = 0; i < fileMetadata.length; i += DEFAULT_SHA_CHECK_BATCH_SIZE) {
|
|
108
|
+
if (signal.aborted) {
|
|
109
|
+
throw createError('Operation cancelled', 'UPLOAD_CANCELLED');
|
|
110
|
+
}
|
|
111
|
+
let batch = fileMetadata.slice(i, i + DEFAULT_SHA_CHECK_BATCH_SIZE);
|
|
112
|
+
let screenshotBatch = batch.map(fileToScreenshotFormat);
|
|
113
|
+
try {
|
|
114
|
+
let res = await checkShas(client, screenshotBatch, buildId);
|
|
115
|
+
let {
|
|
116
|
+
existing = [],
|
|
117
|
+
screenshots = []
|
|
118
|
+
} = res || {};
|
|
119
|
+
for (let sha of existing) {
|
|
120
|
+
existingShas.add(sha);
|
|
121
|
+
}
|
|
122
|
+
allScreenshots.push(...screenshots);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
output.debug('upload', 'SHA check failed, continuing without deduplication', {
|
|
125
|
+
error: error.message
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
let partitioned = partitionFilesByExistence(fileMetadata, existingShas);
|
|
130
|
+
return {
|
|
131
|
+
toUpload: partitioned.toUpload,
|
|
132
|
+
existing: partitioned.existing,
|
|
133
|
+
screenshots: allScreenshots
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// File Upload
|
|
139
|
+
// ============================================================================
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Upload files to Vizzly
|
|
143
|
+
* @param {Object} options - Options
|
|
144
|
+
* @param {Array} options.toUpload - Files to upload
|
|
145
|
+
* @param {string} options.buildId - Build ID
|
|
146
|
+
* @param {Object} options.client - API client
|
|
147
|
+
* @param {AbortSignal} options.signal - Abort signal
|
|
148
|
+
* @param {number} options.batchSize - Batch size
|
|
149
|
+
* @param {Function} options.onProgress - Progress callback
|
|
150
|
+
* @param {Object} options.deps - Dependencies
|
|
151
|
+
* @param {Function} options.deps.createError - Error factory
|
|
152
|
+
* @returns {Promise<{ buildId: string, url: string|null }>}
|
|
153
|
+
*/
|
|
154
|
+
export async function uploadFiles({
|
|
155
|
+
toUpload,
|
|
156
|
+
buildId,
|
|
157
|
+
client,
|
|
158
|
+
signal,
|
|
159
|
+
batchSize,
|
|
160
|
+
onProgress,
|
|
161
|
+
deps
|
|
162
|
+
}) {
|
|
163
|
+
let {
|
|
164
|
+
createError
|
|
165
|
+
} = deps;
|
|
166
|
+
let result = null;
|
|
167
|
+
if (toUpload.length === 0) {
|
|
168
|
+
return {
|
|
169
|
+
buildId,
|
|
170
|
+
url: null
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
for (let i = 0; i < toUpload.length; i += batchSize) {
|
|
174
|
+
if (signal.aborted) {
|
|
175
|
+
throw createError('Operation cancelled', 'UPLOAD_CANCELLED');
|
|
176
|
+
}
|
|
177
|
+
let batch = toUpload.slice(i, i + batchSize);
|
|
178
|
+
let form = new FormData();
|
|
179
|
+
form.append('build_id', buildId);
|
|
180
|
+
for (let file of batch) {
|
|
181
|
+
let blob = new Blob([file.buffer], {
|
|
182
|
+
type: 'image/png'
|
|
183
|
+
});
|
|
184
|
+
form.append('screenshots', blob, file.filename);
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
result = await client.request('/api/sdk/upload', {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
body: form,
|
|
190
|
+
signal,
|
|
191
|
+
headers: {}
|
|
192
|
+
});
|
|
193
|
+
} catch (err) {
|
|
194
|
+
throw createError(`Upload failed: ${err.message}`, 'UPLOAD_FAILED', {
|
|
195
|
+
batch: Math.floor(i / batchSize) + 1
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
onProgress(i + batch.length);
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
buildId,
|
|
202
|
+
url: result?.build?.url || result?.url
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// Build Waiting
|
|
208
|
+
// ============================================================================
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Wait for a build to complete
|
|
212
|
+
* @param {Object} options - Options
|
|
213
|
+
* @param {string} options.buildId - Build ID
|
|
214
|
+
* @param {number} options.timeout - Timeout in ms
|
|
215
|
+
* @param {AbortSignal} options.signal - Abort signal
|
|
216
|
+
* @param {Object} options.client - API client
|
|
217
|
+
* @param {Object} options.deps - Dependencies
|
|
218
|
+
* @param {Function} options.deps.createError - Error factory
|
|
219
|
+
* @param {Function} options.deps.createTimeoutError - Timeout error factory
|
|
220
|
+
* @returns {Promise<Object>} Build result
|
|
221
|
+
*/
|
|
222
|
+
export async function waitForBuild({
|
|
223
|
+
buildId,
|
|
224
|
+
timeout,
|
|
225
|
+
signal,
|
|
226
|
+
client,
|
|
227
|
+
deps
|
|
228
|
+
}) {
|
|
229
|
+
let {
|
|
230
|
+
createError,
|
|
231
|
+
createTimeoutError
|
|
232
|
+
} = deps;
|
|
233
|
+
let startTime = Date.now();
|
|
234
|
+
while (!isTimedOut(startTime, timeout)) {
|
|
235
|
+
if (signal.aborted) {
|
|
236
|
+
throw createError('Operation cancelled', 'UPLOAD_CANCELLED', {
|
|
237
|
+
buildId
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
let resp;
|
|
241
|
+
try {
|
|
242
|
+
resp = await client.request(`/api/sdk/builds/${buildId}`, {
|
|
243
|
+
signal
|
|
244
|
+
});
|
|
245
|
+
} catch (err) {
|
|
246
|
+
let code = extractStatusCodeFromError(err?.message);
|
|
247
|
+
throw createError(`Failed to check build status: ${code}`, 'BUILD_STATUS_FAILED');
|
|
248
|
+
}
|
|
249
|
+
let build = resp?.build ?? resp;
|
|
250
|
+
if (build.status === 'completed') {
|
|
251
|
+
return buildWaitResult(build);
|
|
252
|
+
}
|
|
253
|
+
if (build.status === 'failed') {
|
|
254
|
+
throw createError(`Build failed: ${build.error || 'Unknown error'}`, 'BUILD_FAILED');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
throw createTimeoutError(`Build timed out after ${timeout}ms`, {
|
|
258
|
+
buildId,
|
|
259
|
+
timeout,
|
|
260
|
+
elapsed: getElapsedTime(startTime)
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ============================================================================
|
|
265
|
+
// Main Upload Operation
|
|
266
|
+
// ============================================================================
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Upload screenshots to Vizzly
|
|
270
|
+
* @param {Object} options - Options
|
|
271
|
+
* @param {Object} options.uploadOptions - Upload options (screenshotsDir, buildName, etc.)
|
|
272
|
+
* @param {Object} options.config - Configuration
|
|
273
|
+
* @param {AbortSignal} options.signal - Abort signal
|
|
274
|
+
* @param {number} options.batchSize - Batch size
|
|
275
|
+
* @param {Object} options.deps - Dependencies
|
|
276
|
+
* @returns {Promise<Object>} Upload result
|
|
277
|
+
*/
|
|
278
|
+
export async function upload({
|
|
279
|
+
uploadOptions,
|
|
280
|
+
config,
|
|
281
|
+
signal,
|
|
282
|
+
batchSize,
|
|
283
|
+
deps
|
|
284
|
+
}) {
|
|
285
|
+
let {
|
|
286
|
+
client,
|
|
287
|
+
createBuild,
|
|
288
|
+
getDefaultBranch,
|
|
289
|
+
glob,
|
|
290
|
+
readFile,
|
|
291
|
+
stat,
|
|
292
|
+
checkShas,
|
|
293
|
+
createError,
|
|
294
|
+
createValidationError,
|
|
295
|
+
createUploadError,
|
|
296
|
+
output
|
|
297
|
+
} = deps;
|
|
298
|
+
let {
|
|
299
|
+
screenshotsDir,
|
|
300
|
+
onProgress = () => {}
|
|
301
|
+
} = uploadOptions;
|
|
302
|
+
try {
|
|
303
|
+
// Validate API key
|
|
304
|
+
let apiKeyValidation = validateApiKey(config.apiKey);
|
|
305
|
+
if (!apiKeyValidation.valid) {
|
|
306
|
+
throw createValidationError(apiKeyValidation.error, {
|
|
307
|
+
config: {
|
|
308
|
+
apiKey: config.apiKey,
|
|
309
|
+
apiUrl: config.apiUrl
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Validate screenshots directory
|
|
315
|
+
let dirValidation = validateScreenshotsDir(screenshotsDir);
|
|
316
|
+
if (!dirValidation.valid) {
|
|
317
|
+
throw createValidationError(dirValidation.error);
|
|
318
|
+
}
|
|
319
|
+
let stats = await stat(screenshotsDir);
|
|
320
|
+
let statsValidation = validateDirectoryStats(stats, screenshotsDir);
|
|
321
|
+
if (!statsValidation.valid) {
|
|
322
|
+
throw createValidationError(statsValidation.error);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Find screenshots
|
|
326
|
+
let files = await findScreenshots({
|
|
327
|
+
directory: screenshotsDir,
|
|
328
|
+
deps: {
|
|
329
|
+
glob
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
let filesValidation = validateFilesFound(files, screenshotsDir);
|
|
333
|
+
if (!filesValidation.valid) {
|
|
334
|
+
throw createUploadError(filesValidation.error, filesValidation.context);
|
|
335
|
+
}
|
|
336
|
+
onProgress(buildScanningProgress(files.length));
|
|
337
|
+
|
|
338
|
+
// Process files
|
|
339
|
+
let fileMetadata = await processFiles({
|
|
340
|
+
files,
|
|
341
|
+
signal,
|
|
342
|
+
onProgress: current => onProgress(buildProcessingProgress(current, files.length)),
|
|
343
|
+
deps: {
|
|
344
|
+
readFile,
|
|
345
|
+
createError
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Create build
|
|
350
|
+
let defaultBranch = await getDefaultBranch();
|
|
351
|
+
let buildInfo = buildBuildInfo(uploadOptions, defaultBranch);
|
|
352
|
+
let build = await createBuild(client, buildInfo);
|
|
353
|
+
let buildId = build.id;
|
|
354
|
+
|
|
355
|
+
// Check existing files
|
|
356
|
+
let {
|
|
357
|
+
toUpload,
|
|
358
|
+
existing,
|
|
359
|
+
screenshots
|
|
360
|
+
} = await checkExistingFiles({
|
|
361
|
+
fileMetadata,
|
|
362
|
+
client,
|
|
363
|
+
signal,
|
|
364
|
+
buildId,
|
|
365
|
+
deps: {
|
|
366
|
+
checkShas,
|
|
367
|
+
createError,
|
|
368
|
+
output
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
onProgress(buildDeduplicationProgress(toUpload.length, existing.length, files.length));
|
|
372
|
+
|
|
373
|
+
// Upload files
|
|
374
|
+
let result = await uploadFiles({
|
|
375
|
+
toUpload,
|
|
376
|
+
existing,
|
|
377
|
+
screenshots,
|
|
378
|
+
buildId,
|
|
379
|
+
buildInfo,
|
|
380
|
+
client,
|
|
381
|
+
signal,
|
|
382
|
+
batchSize,
|
|
383
|
+
onProgress: current => onProgress(buildUploadingProgress(current, toUpload.length)),
|
|
384
|
+
deps: {
|
|
385
|
+
createError
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
onProgress(buildCompletedProgress(result.buildId, result.url));
|
|
389
|
+
return buildUploadResult({
|
|
390
|
+
buildId: result.buildId,
|
|
391
|
+
url: result.url,
|
|
392
|
+
total: files.length,
|
|
393
|
+
uploaded: toUpload.length,
|
|
394
|
+
skipped: existing.length
|
|
395
|
+
});
|
|
396
|
+
} catch (error) {
|
|
397
|
+
output.debug('upload', 'failed', {
|
|
398
|
+
error: error.message
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Re-throw if already a VizzlyError
|
|
402
|
+
if (error.name?.includes('Error') && error.code) {
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Wrap unknown errors
|
|
407
|
+
throw createUploadError(`Upload failed: ${error.message}`, {
|
|
408
|
+
originalError: error.message,
|
|
409
|
+
stack: error.stack
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
package/dist/utils/colors.js
CHANGED
|
@@ -1,66 +1,214 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Detects terminal color support and
|
|
1
|
+
// Color utility using ansis for rich terminal styling.
|
|
2
|
+
// Detects terminal color support and provides chainable color functions.
|
|
3
3
|
|
|
4
|
+
import ansis from 'ansis';
|
|
5
|
+
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// Vizzly Observatory Design System Colors
|
|
8
|
+
// Aligned with @vizzly-testing/observatory color tokens
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
export let brand = {
|
|
12
|
+
// Primary brand color - Amber is Observatory's signature
|
|
13
|
+
amber: '#F59E0B',
|
|
14
|
+
// Primary brand, actions, highlights
|
|
15
|
+
amberLight: '#FBBF24',
|
|
16
|
+
// Hover states, emphasis
|
|
17
|
+
|
|
18
|
+
// Accent colors (semantic)
|
|
19
|
+
success: '#10B981',
|
|
20
|
+
// Approved, passed, active (--accent-success)
|
|
21
|
+
warning: '#F59E0B',
|
|
22
|
+
// Pending, attention (--accent-warning)
|
|
23
|
+
danger: '#EF4444',
|
|
24
|
+
// Rejected, failed, errors (--accent-danger)
|
|
25
|
+
info: '#3B82F6',
|
|
26
|
+
// Processing, informational (--accent-info)
|
|
27
|
+
|
|
28
|
+
// Surface colors (dark theme)
|
|
29
|
+
bg: '#0F172A',
|
|
30
|
+
// Page background (--vz-bg)
|
|
31
|
+
surface: '#1A2332',
|
|
32
|
+
// Cards, panels (--vz-surface)
|
|
33
|
+
elevated: '#1E293B',
|
|
34
|
+
// Dropdowns, modals (--vz-elevated)
|
|
35
|
+
border: '#374151',
|
|
36
|
+
// Primary borders (--vz-border)
|
|
37
|
+
borderSubtle: '#2D3748',
|
|
38
|
+
// Subtle dividers (--vz-border-subtle)
|
|
39
|
+
|
|
40
|
+
// Text hierarchy
|
|
41
|
+
textPrimary: '#FFFFFF',
|
|
42
|
+
// Headings, important (--text-primary)
|
|
43
|
+
textSecondary: '#9CA3AF',
|
|
44
|
+
// Body text (--text-secondary)
|
|
45
|
+
textTertiary: '#6B7280',
|
|
46
|
+
// Captions, metadata (--text-tertiary)
|
|
47
|
+
textMuted: '#4B5563',
|
|
48
|
+
// Disabled, placeholders (--text-muted)
|
|
49
|
+
|
|
50
|
+
// Legacy aliases (for backward compatibility)
|
|
51
|
+
green: '#10B981',
|
|
52
|
+
red: '#EF4444',
|
|
53
|
+
cyan: '#06B6D4',
|
|
54
|
+
// Still useful for links in terminals
|
|
55
|
+
slate: '#64748B',
|
|
56
|
+
dark: '#1E293B'
|
|
57
|
+
};
|
|
4
58
|
function supportsColorDefault() {
|
|
5
59
|
// Respect NO_COLOR: https://no-color.org/
|
|
6
60
|
if ('NO_COLOR' in process.env) return false;
|
|
7
61
|
|
|
8
62
|
// Respect FORCE_COLOR if set to a truthy value (except '0')
|
|
9
63
|
if ('FORCE_COLOR' in process.env) {
|
|
10
|
-
|
|
64
|
+
let v = process.env.FORCE_COLOR;
|
|
11
65
|
if (v && v !== '0') return true;
|
|
12
66
|
if (v === '0') return false;
|
|
13
67
|
}
|
|
14
68
|
|
|
69
|
+
// COLORTERM indicates truecolor support
|
|
70
|
+
if (process.env.COLORTERM === 'truecolor' || process.env.COLORTERM === '24bit') {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
15
74
|
// If stdout is not a TTY, assume no color
|
|
16
75
|
if (!process.stdout || !process.stdout.isTTY) return false;
|
|
17
76
|
|
|
18
77
|
// Prefer getColorDepth when available
|
|
19
78
|
try {
|
|
20
|
-
|
|
79
|
+
let depth = typeof process.stdout.getColorDepth === 'function' ? process.stdout.getColorDepth() : 1;
|
|
21
80
|
return depth && depth > 1;
|
|
22
81
|
} catch {
|
|
23
82
|
// Fallback heuristic
|
|
24
83
|
return true;
|
|
25
84
|
}
|
|
26
85
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create a colors API with optional color support detection
|
|
89
|
+
* @param {Object} options - Configuration options
|
|
90
|
+
* @param {boolean} [options.useColor] - Force color on/off (auto-detect if undefined)
|
|
91
|
+
* @returns {Object} Colors API with styling functions
|
|
92
|
+
*/
|
|
34
93
|
export function createColors(options = {}) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
94
|
+
let enabled = options.useColor !== undefined ? !!options.useColor : supportsColorDefault();
|
|
95
|
+
if (!enabled) {
|
|
96
|
+
// Return no-op functions when color disabled
|
|
97
|
+
let noop = (input = '') => String(input);
|
|
98
|
+
return {
|
|
99
|
+
// Modifiers
|
|
100
|
+
reset: noop,
|
|
101
|
+
bold: noop,
|
|
102
|
+
dim: noop,
|
|
103
|
+
italic: noop,
|
|
104
|
+
underline: noop,
|
|
105
|
+
strikethrough: noop,
|
|
106
|
+
// Colors
|
|
107
|
+
red: noop,
|
|
108
|
+
green: noop,
|
|
109
|
+
yellow: noop,
|
|
110
|
+
blue: noop,
|
|
111
|
+
magenta: noop,
|
|
112
|
+
cyan: noop,
|
|
113
|
+
white: noop,
|
|
114
|
+
gray: noop,
|
|
115
|
+
black: noop,
|
|
116
|
+
// Semantic aliases
|
|
117
|
+
success: noop,
|
|
118
|
+
error: noop,
|
|
119
|
+
warning: noop,
|
|
120
|
+
info: noop,
|
|
121
|
+
// Extended colors (return noop factory for chaining)
|
|
122
|
+
rgb: () => noop,
|
|
123
|
+
hex: () => noop,
|
|
124
|
+
bgRgb: () => noop,
|
|
125
|
+
bgHex: () => noop,
|
|
126
|
+
// Observatory brand colors (noop versions)
|
|
127
|
+
brand: {
|
|
128
|
+
// Primary
|
|
129
|
+
amber: noop,
|
|
130
|
+
amberLight: noop,
|
|
131
|
+
// Semantic accents
|
|
132
|
+
success: noop,
|
|
133
|
+
warning: noop,
|
|
134
|
+
danger: noop,
|
|
135
|
+
info: noop,
|
|
136
|
+
// Text hierarchy
|
|
137
|
+
textPrimary: noop,
|
|
138
|
+
textSecondary: noop,
|
|
139
|
+
textTertiary: noop,
|
|
140
|
+
textMuted: noop,
|
|
141
|
+
// Background variants
|
|
142
|
+
bgAmber: noop,
|
|
143
|
+
bgSuccess: noop,
|
|
144
|
+
bgWarning: noop,
|
|
145
|
+
bgDanger: noop,
|
|
146
|
+
bgInfo: noop,
|
|
147
|
+
// Legacy aliases
|
|
148
|
+
green: noop,
|
|
149
|
+
red: noop,
|
|
150
|
+
cyan: noop,
|
|
151
|
+
slate: noop
|
|
152
|
+
}
|
|
153
|
+
};
|
|
55
154
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
155
|
+
return {
|
|
156
|
+
// Modifiers
|
|
157
|
+
reset: ansis.reset,
|
|
158
|
+
bold: ansis.bold,
|
|
159
|
+
dim: ansis.dim,
|
|
160
|
+
italic: ansis.italic,
|
|
161
|
+
underline: ansis.underline,
|
|
162
|
+
strikethrough: ansis.strikethrough,
|
|
163
|
+
// Basic ANSI colors (fallback)
|
|
164
|
+
red: ansis.red,
|
|
165
|
+
green: ansis.green,
|
|
166
|
+
yellow: ansis.yellow,
|
|
167
|
+
blue: ansis.blue,
|
|
168
|
+
magenta: ansis.magenta,
|
|
169
|
+
cyan: ansis.cyan,
|
|
170
|
+
white: ansis.white,
|
|
171
|
+
gray: ansis.gray,
|
|
172
|
+
black: ansis.black,
|
|
173
|
+
// Semantic aliases (basic)
|
|
174
|
+
success: ansis.green,
|
|
175
|
+
error: ansis.red,
|
|
176
|
+
warning: ansis.yellow,
|
|
177
|
+
info: ansis.blue,
|
|
178
|
+
// Extended colors for rich styling
|
|
179
|
+
rgb: ansis.rgb,
|
|
180
|
+
hex: ansis.hex,
|
|
181
|
+
bgRgb: ansis.bgRgb,
|
|
182
|
+
bgHex: ansis.bgHex,
|
|
183
|
+
// Observatory brand colors (Truecolor) - aligned with design system
|
|
184
|
+
brand: {
|
|
185
|
+
// Primary brand color
|
|
186
|
+
amber: ansis.hex(brand.amber),
|
|
187
|
+
amberLight: ansis.hex(brand.amberLight),
|
|
188
|
+
// Semantic accents
|
|
189
|
+
success: ansis.hex(brand.success),
|
|
190
|
+
warning: ansis.hex(brand.warning),
|
|
191
|
+
danger: ansis.hex(brand.danger),
|
|
192
|
+
info: ansis.hex(brand.info),
|
|
193
|
+
// Text hierarchy
|
|
194
|
+
textPrimary: ansis.hex(brand.textPrimary),
|
|
195
|
+
textSecondary: ansis.hex(brand.textSecondary),
|
|
196
|
+
textTertiary: ansis.hex(brand.textTertiary),
|
|
197
|
+
textMuted: ansis.hex(brand.textMuted),
|
|
198
|
+
// Background variants
|
|
199
|
+
bgAmber: ansis.bgHex(brand.amber),
|
|
200
|
+
bgSuccess: ansis.bgHex(brand.success),
|
|
201
|
+
bgWarning: ansis.bgHex(brand.warning),
|
|
202
|
+
bgDanger: ansis.bgHex(brand.danger),
|
|
203
|
+
bgInfo: ansis.bgHex(brand.info),
|
|
204
|
+
// Legacy aliases (backward compatibility)
|
|
205
|
+
green: ansis.hex(brand.green),
|
|
206
|
+
red: ansis.hex(brand.red),
|
|
207
|
+
cyan: ansis.hex(brand.cyan),
|
|
208
|
+
slate: ansis.hex(brand.slate)
|
|
209
|
+
}
|
|
210
|
+
};
|
|
63
211
|
}
|
|
64
212
|
|
|
65
213
|
// Default export with auto-detected color support
|
|
66
|
-
export
|
|
214
|
+
export let colors = createColors();
|
|
@@ -91,10 +91,7 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
91
91
|
config.apiKey = token;
|
|
92
92
|
config.projectSlug = projectMapping.projectSlug;
|
|
93
93
|
config.organizationSlug = projectMapping.organizationSlug;
|
|
94
|
-
output.debug('
|
|
95
|
-
project: projectMapping.projectSlug,
|
|
96
|
-
org: projectMapping.organizationSlug
|
|
97
|
-
});
|
|
94
|
+
output.debug('config', `linked to ${projectMapping.projectSlug} (${projectMapping.organizationSlug})`);
|
|
98
95
|
}
|
|
99
96
|
}
|
|
100
97
|
|
|
@@ -104,14 +101,14 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
104
101
|
const envParallelId = getParallelId();
|
|
105
102
|
if (envApiKey) {
|
|
106
103
|
config.apiKey = envApiKey;
|
|
107
|
-
output.debug('
|
|
104
|
+
output.debug('config', 'using token from environment');
|
|
108
105
|
}
|
|
109
106
|
if (envApiUrl !== 'https://app.vizzly.dev') config.apiUrl = envApiUrl;
|
|
110
107
|
if (envParallelId) config.parallelId = envParallelId;
|
|
111
108
|
|
|
112
109
|
// 5. Apply CLI overrides (highest priority)
|
|
113
110
|
if (cliOverrides.token) {
|
|
114
|
-
output.debug('
|
|
111
|
+
output.debug('config', 'using token from --token flag');
|
|
115
112
|
}
|
|
116
113
|
applyCLIOverrides(config, cliOverrides);
|
|
117
114
|
return config;
|