@vizzly-testing/cli 0.29.6 → 0.31.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 +34 -6
- package/dist/api/client.js +6 -3
- package/dist/api/endpoints.js +59 -0
- package/dist/api/index.js +1 -1
- package/dist/auth/core.js +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/cli.js +206 -51
- package/dist/client/index.js +21 -29
- package/dist/commands/api.js +145 -90
- package/dist/commands/baselines.js +78 -47
- package/dist/commands/builds.js +9 -8
- package/dist/commands/comparisons.js +7 -8
- package/dist/commands/config-cmd.js +15 -7
- package/dist/commands/context.js +870 -0
- package/dist/commands/doctor.js +116 -81
- package/dist/commands/init.js +202 -201
- package/dist/commands/login.js +134 -65
- package/dist/commands/logout.js +18 -7
- package/dist/commands/preview.js +29 -57
- package/dist/commands/project.js +120 -0
- package/dist/commands/projects.js +9 -2
- package/dist/commands/review.js +236 -202
- package/dist/commands/run.js +75 -72
- package/dist/commands/status.js +228 -150
- package/dist/commands/tdd-daemon-process.js +98 -0
- package/dist/commands/tdd-daemon.js +318 -212
- package/dist/commands/tdd.js +23 -8
- package/dist/commands/upload.js +10 -9
- package/dist/commands/whoami.js +64 -26
- package/dist/config/core.js +37 -36
- package/dist/context/local-workspace-provider.js +532 -0
- package/dist/context/provider-resolver.js +36 -0
- package/dist/index.js +5 -4
- package/dist/plugin-loader.js +55 -49
- package/dist/project/core.js +2 -2
- package/dist/project/operations.js +6 -3
- package/dist/reporter/reporter-bundle.css +2 -1
- package/dist/reporter/reporter-bundle.iife.js +119 -119
- package/dist/reporter-ssr/ssr-entry.js +605 -509
- package/dist/sdk/index.js +162 -107
- package/dist/server/handlers/tdd-handler.js +30 -54
- package/dist/server/routers/assets.js +4 -0
- package/dist/server/routers/baseline.js +195 -178
- package/dist/server/routers/events.js +226 -148
- package/dist/server/routers/projects.js +14 -5
- package/dist/server/routers/screenshot.js +1 -31
- package/dist/services/build-manager.js +19 -97
- package/dist/services/config-service.js +26 -34
- package/dist/services/index.js +2 -4
- package/dist/services/static-report-generator.js +11 -12
- package/dist/services/test-runner.js +21 -27
- package/dist/tdd/core/hotspot-coverage.js +1 -1
- package/dist/tdd/index.js +1 -1
- package/dist/tdd/metadata/region-metadata.js +0 -37
- package/dist/tdd/runtime-context.js +78 -0
- package/dist/tdd/server-registry.js +38 -17
- package/dist/tdd/services/baseline-manager.js +23 -0
- package/dist/tdd/services/hotspot-service.js +1 -14
- package/dist/tdd/tdd-service.js +82 -113
- package/dist/types/client.d.ts +36 -1
- package/dist/types/index.d.ts +114 -22
- package/dist/types/sdk.d.ts +47 -7
- package/dist/uploader/core.js +5 -4
- package/dist/uploader/index.js +79 -2
- package/dist/uploader/operations.js +40 -5
- package/dist/utils/api-url.js +14 -0
- package/dist/utils/async-utils.js +26 -0
- package/dist/utils/ci-env.js +19 -12
- package/dist/utils/colors.js +46 -44
- package/dist/utils/config-loader.js +39 -65
- package/dist/utils/config-schema.js +19 -55
- package/dist/utils/fetch-utils.js +12 -5
- package/dist/utils/git.js +35 -72
- package/dist/utils/global-config.js +22 -3
- package/dist/utils/output.js +19 -7
- package/dist/utils/patterns.js +14 -0
- package/dist/utils/project-link-store.js +105 -0
- package/dist/utils/report-data.js +31 -0
- package/dist/utils/screenshot-options.js +28 -0
- package/dist/utils/secret-store.js +65 -0
- package/dist/utils/security.js +3 -2
- package/package.json +14 -11
- package/dist/services/screenshot-server.js +0 -65
- package/dist/services/server-manager.js +0 -81
- package/dist/services/uploader.js +0 -100
- package/dist/tdd/services/region-service.js +0 -75
- package/dist/utils/build-history.js +0 -103
package/dist/uploader/index.js
CHANGED
|
@@ -1,11 +1,88 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Uploader Module
|
|
3
3
|
*
|
|
4
|
-
* Exports pure functions (core)
|
|
4
|
+
* Exports pure functions (core), I/O operations, and the public uploader
|
|
5
|
+
* factory for screenshot uploading.
|
|
5
6
|
*/
|
|
6
7
|
|
|
8
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
9
|
+
import { glob } from 'glob';
|
|
10
|
+
import { checkShas, createApiClient, createBuild } from '../api/index.js';
|
|
11
|
+
import { TimeoutError, UploadError, ValidationError } from '../errors/vizzly-error.js';
|
|
12
|
+
import { getDefaultBranch } from '../utils/git.js';
|
|
13
|
+
import * as output from '../utils/output.js';
|
|
14
|
+
import { resolveBatchSize, resolveTimeout } from './core.js';
|
|
15
|
+
import { upload as uploadOperation, waitForBuild as waitForBuildOperation } from './operations.js';
|
|
16
|
+
export function createUploader({
|
|
17
|
+
apiKey,
|
|
18
|
+
apiUrl,
|
|
19
|
+
userAgent,
|
|
20
|
+
command,
|
|
21
|
+
upload: uploadConfig = {}
|
|
22
|
+
} = {}, options = {}) {
|
|
23
|
+
let signal = options.signal || new AbortController().signal;
|
|
24
|
+
let client = createApiClient({
|
|
25
|
+
baseUrl: apiUrl,
|
|
26
|
+
token: apiKey,
|
|
27
|
+
command: command || 'upload',
|
|
28
|
+
sdkUserAgent: userAgent,
|
|
29
|
+
allowNoToken: true
|
|
30
|
+
});
|
|
31
|
+
let batchSize = resolveBatchSize(options, uploadConfig);
|
|
32
|
+
let timeout = resolveTimeout(options, uploadConfig);
|
|
33
|
+
let deps = options.deps || {
|
|
34
|
+
client,
|
|
35
|
+
createBuild,
|
|
36
|
+
getDefaultBranch,
|
|
37
|
+
glob,
|
|
38
|
+
readFile,
|
|
39
|
+
stat,
|
|
40
|
+
checkShas,
|
|
41
|
+
createError: (message, code, context) => {
|
|
42
|
+
let error = new UploadError(message, context);
|
|
43
|
+
error.code = code;
|
|
44
|
+
return error;
|
|
45
|
+
},
|
|
46
|
+
createValidationError: (message, context) => new ValidationError(message, context),
|
|
47
|
+
createUploadError: (message, context) => new UploadError(message, context),
|
|
48
|
+
createTimeoutError: (message, context) => new TimeoutError(message, context),
|
|
49
|
+
output
|
|
50
|
+
};
|
|
51
|
+
async function upload(uploadOptions) {
|
|
52
|
+
return uploadOperation({
|
|
53
|
+
uploadOptions,
|
|
54
|
+
config: {
|
|
55
|
+
apiKey,
|
|
56
|
+
apiUrl
|
|
57
|
+
},
|
|
58
|
+
signal,
|
|
59
|
+
batchSize,
|
|
60
|
+
deps: {
|
|
61
|
+
...deps,
|
|
62
|
+
client: deps.client || client
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function waitForBuild(buildId, waitTimeout = timeout) {
|
|
67
|
+
return waitForBuildOperation({
|
|
68
|
+
buildId,
|
|
69
|
+
timeout: waitTimeout,
|
|
70
|
+
signal,
|
|
71
|
+
client: deps.client || client,
|
|
72
|
+
deps: {
|
|
73
|
+
createError: deps.createError,
|
|
74
|
+
createTimeoutError: deps.createTimeoutError
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
upload,
|
|
80
|
+
waitForBuild
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
7
84
|
// Core - pure functions
|
|
8
|
-
export { buildBuildInfo, buildCompletedProgress, buildDeduplicationProgress, buildFileMetadata, buildProcessingProgress, buildScanningProgress, buildScreenshotPattern, buildUploadingProgress, buildUploadResult, buildWaitResult, computeSha256, DEFAULT_BATCH_SIZE, DEFAULT_SHA_CHECK_BATCH_SIZE, DEFAULT_TIMEOUT, extractBrowserFromFilename, extractStatusCodeFromError, fileToScreenshotFormat, getElapsedTime, isTimedOut, partitionFilesByExistence, resolveBatchSize, resolveTimeout, validateApiKey, validateDirectoryStats, validateFilesFound, validateScreenshotsDir } from './core.js';
|
|
85
|
+
export { buildBuildInfo, buildCompletedProgress, buildDeduplicationProgress, buildFileMetadata, buildProcessingProgress, buildScanningProgress, buildScreenshotPattern, buildUploadingProgress, buildUploadResult, buildWaitResult, computeSha256, DEFAULT_BATCH_SIZE, DEFAULT_BUILD_POLL_INTERVAL, DEFAULT_SHA_CHECK_BATCH_SIZE, DEFAULT_TIMEOUT, extractBrowserFromFilename, extractStatusCodeFromError, fileToScreenshotFormat, getElapsedTime, isTimedOut, partitionFilesByExistence, resolveBatchSize, resolveTimeout, validateApiKey, validateDirectoryStats, validateFilesFound, validateScreenshotsDir } from './core.js';
|
|
9
86
|
|
|
10
87
|
// Operations - I/O with dependency injection
|
|
11
88
|
export { checkExistingFiles, findScreenshots, processFiles, upload, uploadFiles, waitForBuild } from './operations.js';
|
|
@@ -4,7 +4,34 @@
|
|
|
4
4
|
* Each operation takes its dependencies as parameters for testability.
|
|
5
5
|
*/
|
|
6
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';
|
|
7
|
+
import { buildBuildInfo, buildCompletedProgress, buildDeduplicationProgress, buildFileMetadata, buildProcessingProgress, buildScanningProgress, buildScreenshotPattern, buildUploadingProgress, buildUploadResult, buildWaitResult, DEFAULT_BUILD_POLL_INTERVAL, DEFAULT_SHA_CHECK_BATCH_SIZE, extractStatusCodeFromError, fileToScreenshotFormat, getElapsedTime, isTimedOut, partitionFilesByExistence, validateApiKey, validateDirectoryStats, validateFilesFound, validateScreenshotsDir } from './core.js';
|
|
8
|
+
function delay(ms, signal) {
|
|
9
|
+
if (ms <= 0) {
|
|
10
|
+
return Promise.resolve();
|
|
11
|
+
}
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
let timeoutId;
|
|
14
|
+
function cleanup() {
|
|
15
|
+
clearTimeout(timeoutId);
|
|
16
|
+
signal?.removeEventListener?.('abort', handleAbort);
|
|
17
|
+
}
|
|
18
|
+
function handleAbort() {
|
|
19
|
+
cleanup();
|
|
20
|
+
reject(new Error('Operation cancelled'));
|
|
21
|
+
}
|
|
22
|
+
timeoutId = setTimeout(() => {
|
|
23
|
+
cleanup();
|
|
24
|
+
resolve();
|
|
25
|
+
}, ms);
|
|
26
|
+
if (signal?.aborted) {
|
|
27
|
+
handleAbort();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
signal?.addEventListener?.('abort', handleAbort, {
|
|
31
|
+
once: true
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
8
35
|
|
|
9
36
|
// ============================================================================
|
|
10
37
|
// File Discovery
|
|
@@ -228,10 +255,13 @@ export async function waitForBuild({
|
|
|
228
255
|
}) {
|
|
229
256
|
let {
|
|
230
257
|
createError,
|
|
231
|
-
createTimeoutError
|
|
258
|
+
createTimeoutError,
|
|
259
|
+
now = () => Date.now(),
|
|
260
|
+
pollInterval = DEFAULT_BUILD_POLL_INTERVAL,
|
|
261
|
+
wait = delay
|
|
232
262
|
} = deps;
|
|
233
|
-
let startTime =
|
|
234
|
-
while (!isTimedOut(startTime, timeout)) {
|
|
263
|
+
let startTime = now();
|
|
264
|
+
while (!isTimedOut(startTime, timeout, now())) {
|
|
235
265
|
if (signal.aborted) {
|
|
236
266
|
throw createError('Operation cancelled', 'UPLOAD_CANCELLED', {
|
|
237
267
|
buildId
|
|
@@ -253,11 +283,16 @@ export async function waitForBuild({
|
|
|
253
283
|
if (build.status === 'failed') {
|
|
254
284
|
throw createError(`Build failed: ${build.error || 'Unknown error'}`, 'BUILD_FAILED');
|
|
255
285
|
}
|
|
286
|
+
let remaining = timeout - getElapsedTime(startTime, now());
|
|
287
|
+
if (remaining <= 0) {
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
await wait(Math.min(pollInterval, remaining), signal);
|
|
256
291
|
}
|
|
257
292
|
throw createTimeoutError(`Build timed out after ${timeout}ms`, {
|
|
258
293
|
buildId,
|
|
259
294
|
timeout,
|
|
260
|
-
elapsed: getElapsedTime(startTime)
|
|
295
|
+
elapsed: getElapsedTime(startTime, now())
|
|
261
296
|
});
|
|
262
297
|
}
|
|
263
298
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function getAppBaseUrl(apiUrl) {
|
|
2
|
+
try {
|
|
3
|
+
let url = new URL(apiUrl);
|
|
4
|
+
let apiPathIndex = url.pathname.indexOf('/api');
|
|
5
|
+
if (apiPathIndex !== -1) {
|
|
6
|
+
url.pathname = url.pathname.slice(0, apiPathIndex) || '/';
|
|
7
|
+
url.search = '';
|
|
8
|
+
url.hash = '';
|
|
9
|
+
}
|
|
10
|
+
return url.toString().replace(/\/$/, '');
|
|
11
|
+
} catch {
|
|
12
|
+
return apiUrl.replace(/\/api(?:\/.*)?$/, '');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Race an operation against a timeout and always clear the timer when either
|
|
3
|
+
* side settles.
|
|
4
|
+
*
|
|
5
|
+
* @param {Promise<unknown>} operation - Operation promise to wait for.
|
|
6
|
+
* @param {number} ms - Timeout in milliseconds.
|
|
7
|
+
* @param {string} message - Error message when the timeout wins.
|
|
8
|
+
* @param {Object} [timers] - Timer implementation for deterministic tests.
|
|
9
|
+
* @returns {Promise<unknown>} The operation result.
|
|
10
|
+
*/
|
|
11
|
+
export async function withTimeout(operation, ms, message, timers = {
|
|
12
|
+
setTimeout,
|
|
13
|
+
clearTimeout
|
|
14
|
+
}) {
|
|
15
|
+
let timeoutId;
|
|
16
|
+
let timeoutPromise = new Promise((_, reject) => {
|
|
17
|
+
timeoutId = timers.setTimeout(() => reject(new Error(message)), ms);
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
return await Promise.race([operation, timeoutPromise]);
|
|
21
|
+
} finally {
|
|
22
|
+
if (timeoutId !== undefined) {
|
|
23
|
+
timers.clearTimeout(timeoutId);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/utils/ci-env.js
CHANGED
|
@@ -176,6 +176,13 @@ export function getCommitMessage() {
|
|
|
176
176
|
// Generic
|
|
177
177
|
null;
|
|
178
178
|
}
|
|
179
|
+
function parsePullRequestNumber(value) {
|
|
180
|
+
if (!/^\d+$/.test(value)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
let number = Number(value);
|
|
184
|
+
return Number.isSafeInteger(number) && number > 0 ? number : null;
|
|
185
|
+
}
|
|
179
186
|
|
|
180
187
|
/**
|
|
181
188
|
* Get the pull request number from CI environment variables
|
|
@@ -184,54 +191,54 @@ export function getCommitMessage() {
|
|
|
184
191
|
export function getPullRequestNumber() {
|
|
185
192
|
// Check VIZZLY override first
|
|
186
193
|
if (process.env.VIZZLY_PR_NUMBER) {
|
|
187
|
-
return
|
|
194
|
+
return parsePullRequestNumber(process.env.VIZZLY_PR_NUMBER);
|
|
188
195
|
}
|
|
189
196
|
|
|
190
197
|
// GitHub Actions - extract from GITHUB_REF
|
|
191
198
|
if (process.env.GITHUB_ACTIONS && process.env.GITHUB_EVENT_NAME === 'pull_request') {
|
|
192
|
-
|
|
193
|
-
if (prMatch?.[1]) return
|
|
199
|
+
let prMatch = process.env.GITHUB_REF?.match(/refs\/pull\/(\d+)\/merge/);
|
|
200
|
+
if (prMatch?.[1]) return parsePullRequestNumber(prMatch[1]);
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
// GitLab CI
|
|
197
204
|
if (process.env.CI_MERGE_REQUEST_ID) {
|
|
198
|
-
return
|
|
205
|
+
return parsePullRequestNumber(process.env.CI_MERGE_REQUEST_ID);
|
|
199
206
|
}
|
|
200
207
|
|
|
201
208
|
// CircleCI - extract from PR URL
|
|
202
209
|
if (process.env.CIRCLE_PULL_REQUEST) {
|
|
203
|
-
|
|
204
|
-
if (prMatch?.[1]) return
|
|
210
|
+
let prMatch = process.env.CIRCLE_PULL_REQUEST.match(/\/pull\/(\d+)$/);
|
|
211
|
+
if (prMatch?.[1]) return parsePullRequestNumber(prMatch[1]);
|
|
205
212
|
}
|
|
206
213
|
|
|
207
214
|
// Travis CI
|
|
208
215
|
if (process.env.TRAVIS_PULL_REQUEST && process.env.TRAVIS_PULL_REQUEST !== 'false') {
|
|
209
|
-
return
|
|
216
|
+
return parsePullRequestNumber(process.env.TRAVIS_PULL_REQUEST);
|
|
210
217
|
}
|
|
211
218
|
|
|
212
219
|
// Buildkite
|
|
213
220
|
if (process.env.BUILDKITE_PULL_REQUEST && process.env.BUILDKITE_PULL_REQUEST !== 'false') {
|
|
214
|
-
return
|
|
221
|
+
return parsePullRequestNumber(process.env.BUILDKITE_PULL_REQUEST);
|
|
215
222
|
}
|
|
216
223
|
|
|
217
224
|
// Drone CI
|
|
218
225
|
if (process.env.DRONE_PULL_REQUEST) {
|
|
219
|
-
return
|
|
226
|
+
return parsePullRequestNumber(process.env.DRONE_PULL_REQUEST);
|
|
220
227
|
}
|
|
221
228
|
|
|
222
229
|
// Jenkins (GitHub Pull Request Builder plugin)
|
|
223
230
|
if (process.env.ghprbPullId) {
|
|
224
|
-
return
|
|
231
|
+
return parsePullRequestNumber(process.env.ghprbPullId);
|
|
225
232
|
}
|
|
226
233
|
|
|
227
234
|
// Azure DevOps
|
|
228
235
|
if (process.env.SYSTEM_PULLREQUEST_PULLREQUESTID) {
|
|
229
|
-
return
|
|
236
|
+
return parsePullRequestNumber(process.env.SYSTEM_PULLREQUEST_PULLREQUESTID);
|
|
230
237
|
}
|
|
231
238
|
|
|
232
239
|
// AppVeyor
|
|
233
240
|
if (process.env.APPVEYOR_PULL_REQUEST_NUMBER) {
|
|
234
|
-
return
|
|
241
|
+
return parsePullRequestNumber(process.env.APPVEYOR_PULL_REQUEST_NUMBER);
|
|
235
242
|
}
|
|
236
243
|
return null;
|
|
237
244
|
}
|
package/dist/utils/colors.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Color utility using ansis for rich terminal styling.
|
|
2
2
|
// Detects terminal color support and provides chainable color functions.
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import { Ansis } from 'ansis';
|
|
5
5
|
|
|
6
6
|
// =============================================================================
|
|
7
7
|
// Vizzly Observatory Design System Colors
|
|
@@ -72,7 +72,7 @@ function supportsColorDefault() {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// If stdout is not a TTY, assume no color
|
|
75
|
-
if (!process.stdout
|
|
75
|
+
if (!process.stdout?.isTTY) return false;
|
|
76
76
|
|
|
77
77
|
// Prefer getColorDepth when available
|
|
78
78
|
try {
|
|
@@ -92,6 +92,7 @@ function supportsColorDefault() {
|
|
|
92
92
|
*/
|
|
93
93
|
export function createColors(options = {}) {
|
|
94
94
|
let enabled = options.useColor !== undefined ? !!options.useColor : supportsColorDefault();
|
|
95
|
+
let level = options.useColor !== undefined ? enabled ? 3 : 0 : enabled ? 3 : 0;
|
|
95
96
|
if (!enabled) {
|
|
96
97
|
// Return no-op functions when color disabled
|
|
97
98
|
let noop = (input = '') => String(input);
|
|
@@ -152,60 +153,61 @@ export function createColors(options = {}) {
|
|
|
152
153
|
}
|
|
153
154
|
};
|
|
154
155
|
}
|
|
156
|
+
let colors = new Ansis(level);
|
|
155
157
|
return {
|
|
156
158
|
// Modifiers
|
|
157
|
-
reset:
|
|
158
|
-
bold:
|
|
159
|
-
dim:
|
|
160
|
-
italic:
|
|
161
|
-
underline:
|
|
162
|
-
strikethrough:
|
|
159
|
+
reset: colors.reset,
|
|
160
|
+
bold: colors.bold,
|
|
161
|
+
dim: colors.dim,
|
|
162
|
+
italic: colors.italic,
|
|
163
|
+
underline: colors.underline,
|
|
164
|
+
strikethrough: colors.strikethrough,
|
|
163
165
|
// Basic ANSI colors (fallback)
|
|
164
|
-
red:
|
|
165
|
-
green:
|
|
166
|
-
yellow:
|
|
167
|
-
blue:
|
|
168
|
-
magenta:
|
|
169
|
-
cyan:
|
|
170
|
-
white:
|
|
171
|
-
gray:
|
|
172
|
-
black:
|
|
166
|
+
red: colors.red,
|
|
167
|
+
green: colors.green,
|
|
168
|
+
yellow: colors.yellow,
|
|
169
|
+
blue: colors.blue,
|
|
170
|
+
magenta: colors.magenta,
|
|
171
|
+
cyan: colors.cyan,
|
|
172
|
+
white: colors.white,
|
|
173
|
+
gray: colors.gray,
|
|
174
|
+
black: colors.black,
|
|
173
175
|
// Semantic aliases (basic)
|
|
174
|
-
success:
|
|
175
|
-
error:
|
|
176
|
-
warning:
|
|
177
|
-
info:
|
|
176
|
+
success: colors.green,
|
|
177
|
+
error: colors.red,
|
|
178
|
+
warning: colors.yellow,
|
|
179
|
+
info: colors.blue,
|
|
178
180
|
// Extended colors for rich styling
|
|
179
|
-
rgb:
|
|
180
|
-
hex:
|
|
181
|
-
bgRgb:
|
|
182
|
-
bgHex:
|
|
181
|
+
rgb: colors.rgb.bind(colors),
|
|
182
|
+
hex: colors.hex.bind(colors),
|
|
183
|
+
bgRgb: colors.bgRgb.bind(colors),
|
|
184
|
+
bgHex: colors.bgHex.bind(colors),
|
|
183
185
|
// Observatory brand colors (Truecolor) - aligned with design system
|
|
184
186
|
brand: {
|
|
185
187
|
// Primary brand color
|
|
186
|
-
amber:
|
|
187
|
-
amberLight:
|
|
188
|
+
amber: colors.hex(brand.amber),
|
|
189
|
+
amberLight: colors.hex(brand.amberLight),
|
|
188
190
|
// Semantic accents
|
|
189
|
-
success:
|
|
190
|
-
warning:
|
|
191
|
-
danger:
|
|
192
|
-
info:
|
|
191
|
+
success: colors.hex(brand.success),
|
|
192
|
+
warning: colors.hex(brand.warning),
|
|
193
|
+
danger: colors.hex(brand.danger),
|
|
194
|
+
info: colors.hex(brand.info),
|
|
193
195
|
// Text hierarchy
|
|
194
|
-
textPrimary:
|
|
195
|
-
textSecondary:
|
|
196
|
-
textTertiary:
|
|
197
|
-
textMuted:
|
|
196
|
+
textPrimary: colors.hex(brand.textPrimary),
|
|
197
|
+
textSecondary: colors.hex(brand.textSecondary),
|
|
198
|
+
textTertiary: colors.hex(brand.textTertiary),
|
|
199
|
+
textMuted: colors.hex(brand.textMuted),
|
|
198
200
|
// Background variants
|
|
199
|
-
bgAmber:
|
|
200
|
-
bgSuccess:
|
|
201
|
-
bgWarning:
|
|
202
|
-
bgDanger:
|
|
203
|
-
bgInfo:
|
|
201
|
+
bgAmber: colors.bgHex(brand.amber),
|
|
202
|
+
bgSuccess: colors.bgHex(brand.success),
|
|
203
|
+
bgWarning: colors.bgHex(brand.warning),
|
|
204
|
+
bgDanger: colors.bgHex(brand.danger),
|
|
205
|
+
bgInfo: colors.bgHex(brand.info),
|
|
204
206
|
// Legacy aliases (backward compatibility)
|
|
205
|
-
green:
|
|
206
|
-
red:
|
|
207
|
-
cyan:
|
|
208
|
-
slate:
|
|
207
|
+
green: colors.hex(brand.green),
|
|
208
|
+
red: colors.hex(brand.red),
|
|
209
|
+
cyan: colors.hex(brand.cyan),
|
|
210
|
+
slate: colors.hex(brand.slate)
|
|
209
211
|
}
|
|
210
212
|
};
|
|
211
213
|
}
|
|
@@ -1,45 +1,15 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { cosmiconfigSync } from 'cosmiconfig';
|
|
3
|
+
import { CONFIG_DEFAULTS, deepMerge } from '../config/core.js';
|
|
3
4
|
import { validateVizzlyConfigWithDefaults } from './config-schema.js';
|
|
4
5
|
import { getApiToken, getApiUrl, getBuildName, getParallelId } from './environment-config.js';
|
|
5
6
|
import { getAccessToken } from './global-config.js';
|
|
6
7
|
import * as output from './output.js';
|
|
7
|
-
|
|
8
|
-
// API Configuration
|
|
9
|
-
apiKey: undefined,
|
|
10
|
-
// Will be set from env, global config, or CLI overrides
|
|
11
|
-
apiUrl: getApiUrl(),
|
|
12
|
-
// Server Configuration (for run command)
|
|
13
|
-
server: {
|
|
14
|
-
port: 47392,
|
|
15
|
-
timeout: 30000
|
|
16
|
-
},
|
|
17
|
-
// Build Configuration
|
|
18
|
-
build: {
|
|
19
|
-
name: 'Build {timestamp}',
|
|
20
|
-
environment: 'test'
|
|
21
|
-
},
|
|
22
|
-
// Upload Configuration (for upload command)
|
|
23
|
-
upload: {
|
|
24
|
-
screenshotsDir: './screenshots',
|
|
25
|
-
batchSize: 10,
|
|
26
|
-
timeout: 30000
|
|
27
|
-
},
|
|
28
|
-
// Comparison Configuration (CIEDE2000 Delta E: 0=exact, 1=JND, 2=recommended)
|
|
29
|
-
comparison: {
|
|
30
|
-
threshold: 2.0
|
|
31
|
-
},
|
|
32
|
-
// TDD Configuration
|
|
33
|
-
tdd: {
|
|
34
|
-
openReport: false // Whether to auto-open HTML report in browser
|
|
35
|
-
},
|
|
36
|
-
// Plugins
|
|
37
|
-
plugins: []
|
|
38
|
-
};
|
|
8
|
+
import { getActiveProjectLink } from './project-link-store.js';
|
|
39
9
|
export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
40
10
|
// 1. Load from config file using cosmiconfig
|
|
41
|
-
|
|
42
|
-
|
|
11
|
+
let explorer = cosmiconfigSync('vizzly');
|
|
12
|
+
let result = configPath ? explorer.load(configPath) : explorer.search();
|
|
43
13
|
let fileConfig = {};
|
|
44
14
|
if (result?.config) {
|
|
45
15
|
// Handle ESM default export (cosmiconfig wraps it in { default: {...} })
|
|
@@ -47,28 +17,10 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
47
17
|
}
|
|
48
18
|
|
|
49
19
|
// 2. Validate config file using Zod schema
|
|
50
|
-
|
|
20
|
+
let validatedFileConfig = validateVizzlyConfigWithDefaults(fileConfig);
|
|
51
21
|
|
|
52
22
|
// Create a proper clone of the default config to avoid shared object references
|
|
53
|
-
|
|
54
|
-
...DEFAULT_CONFIG,
|
|
55
|
-
server: {
|
|
56
|
-
...DEFAULT_CONFIG.server
|
|
57
|
-
},
|
|
58
|
-
build: {
|
|
59
|
-
...DEFAULT_CONFIG.build
|
|
60
|
-
},
|
|
61
|
-
upload: {
|
|
62
|
-
...DEFAULT_CONFIG.upload
|
|
63
|
-
},
|
|
64
|
-
comparison: {
|
|
65
|
-
...DEFAULT_CONFIG.comparison
|
|
66
|
-
},
|
|
67
|
-
tdd: {
|
|
68
|
-
...DEFAULT_CONFIG.tdd
|
|
69
|
-
},
|
|
70
|
-
plugins: [...DEFAULT_CONFIG.plugins]
|
|
71
|
-
};
|
|
23
|
+
let config = deepMerge(CONFIG_DEFAULTS, {});
|
|
72
24
|
|
|
73
25
|
// Merge validated file config
|
|
74
26
|
mergeConfig(config, validatedFileConfig);
|
|
@@ -95,16 +47,35 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
95
47
|
}
|
|
96
48
|
applyCLIOverrides(config, cliOverrides);
|
|
97
49
|
|
|
98
|
-
// 5. Fall back to
|
|
99
|
-
// This enables interactive commands (builds, comparisons, approve, etc.)
|
|
100
|
-
// to work without a project token when the user is logged in
|
|
50
|
+
// 5. Fall back to a linked project token for cloud upload commands.
|
|
101
51
|
if (!config.apiKey) {
|
|
102
|
-
let
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
52
|
+
let linkedProject = await getActiveProjectLink({
|
|
53
|
+
apiUrl: config.apiUrl
|
|
54
|
+
});
|
|
55
|
+
if (linkedProject?.token) {
|
|
56
|
+
config.apiKey = linkedProject.token;
|
|
57
|
+
config.linkedProject = {
|
|
58
|
+
account: linkedProject.account,
|
|
59
|
+
apiUrl: linkedProject.apiUrl,
|
|
60
|
+
organizationSlug: linkedProject.organizationSlug,
|
|
61
|
+
organizationName: linkedProject.organizationName,
|
|
62
|
+
projectSlug: linkedProject.projectSlug,
|
|
63
|
+
projectName: linkedProject.projectName,
|
|
64
|
+
tokenId: linkedProject.tokenId,
|
|
65
|
+
tokenPrefix: linkedProject.tokenPrefix,
|
|
66
|
+
expiresAt: linkedProject.expiresAt,
|
|
67
|
+
storage: linkedProject.storage
|
|
68
|
+
};
|
|
69
|
+
output.debug('config', 'using linked project token');
|
|
106
70
|
}
|
|
107
71
|
}
|
|
72
|
+
|
|
73
|
+
// 6. Keep user auth separate from upload credentials.
|
|
74
|
+
let userToken = await getAccessToken();
|
|
75
|
+
if (userToken) {
|
|
76
|
+
config.userToken = userToken;
|
|
77
|
+
output.debug('config', 'using user login for user-authenticated commands');
|
|
78
|
+
}
|
|
108
79
|
return config;
|
|
109
80
|
}
|
|
110
81
|
|
|
@@ -126,19 +97,22 @@ function applyCLIOverrides(config, cliOverrides = {}) {
|
|
|
126
97
|
if (cliOverrides.parallelId) config.parallelId = cliOverrides.parallelId;
|
|
127
98
|
|
|
128
99
|
// Server overrides
|
|
129
|
-
if (cliOverrides.port) config.server.port =
|
|
130
|
-
if (cliOverrides.timeout) config.server.timeout =
|
|
100
|
+
if (cliOverrides.port) config.server.port = Number(cliOverrides.port);
|
|
101
|
+
if (cliOverrides.timeout) config.server.timeout = Number(cliOverrides.timeout);
|
|
131
102
|
|
|
132
103
|
// Upload overrides
|
|
133
104
|
if (cliOverrides.batchSize !== undefined) {
|
|
134
|
-
config.upload.batchSize =
|
|
105
|
+
config.upload.batchSize = Number(cliOverrides.batchSize);
|
|
135
106
|
}
|
|
136
107
|
if (cliOverrides.uploadTimeout !== undefined) {
|
|
137
|
-
config.upload.timeout =
|
|
108
|
+
config.upload.timeout = Number(cliOverrides.uploadTimeout);
|
|
138
109
|
}
|
|
139
110
|
|
|
140
111
|
// Comparison overrides
|
|
141
112
|
if (cliOverrides.threshold !== undefined) config.comparison.threshold = cliOverrides.threshold;
|
|
113
|
+
if (cliOverrides.minClusterSize !== undefined) {
|
|
114
|
+
config.comparison.minClusterSize = Number(cliOverrides.minClusterSize);
|
|
115
|
+
}
|
|
142
116
|
|
|
143
117
|
// Baseline overrides
|
|
144
118
|
if (cliOverrides.baselineBuild) config.baselineBuildId = cliOverrides.baselineBuild;
|