@vizzly-testing/cli 0.17.0 → 0.19.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/dist/cli.js +87 -59
- package/dist/client/index.js +6 -6
- package/dist/commands/doctor.js +15 -15
- package/dist/commands/finalize.js +7 -7
- package/dist/commands/init.js +28 -28
- package/dist/commands/login.js +23 -23
- package/dist/commands/logout.js +4 -4
- package/dist/commands/project.js +36 -36
- package/dist/commands/run.js +33 -33
- package/dist/commands/status.js +14 -14
- package/dist/commands/tdd-daemon.js +43 -43
- package/dist/commands/tdd.js +26 -26
- package/dist/commands/upload.js +32 -32
- package/dist/commands/whoami.js +12 -12
- package/dist/index.js +9 -14
- package/dist/plugin-api.js +43 -0
- package/dist/plugin-loader.js +28 -28
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +19 -19
- package/dist/sdk/index.js +33 -35
- package/dist/server/handlers/api-handler.js +4 -4
- package/dist/server/handlers/tdd-handler.js +22 -21
- package/dist/server/http-server.js +21 -22
- package/dist/server/middleware/json-parser.js +1 -1
- package/dist/server/routers/assets.js +14 -14
- package/dist/server/routers/auth.js +14 -14
- package/dist/server/routers/baseline.js +8 -8
- package/dist/server/routers/cloud-proxy.js +15 -15
- package/dist/server/routers/config.js +11 -11
- package/dist/server/routers/dashboard.js +11 -11
- package/dist/server/routers/health.js +4 -4
- package/dist/server/routers/projects.js +19 -19
- package/dist/server/routers/screenshot.js +9 -9
- package/dist/services/api-service.js +16 -16
- package/dist/services/auth-service.js +17 -17
- package/dist/services/build-manager.js +3 -3
- package/dist/services/config-service.js +32 -32
- package/dist/services/html-report-generator.js +8 -8
- package/dist/services/index.js +11 -11
- package/dist/services/project-service.js +19 -19
- package/dist/services/report-generator/report.css +3 -3
- package/dist/services/report-generator/viewer.js +25 -23
- package/dist/services/screenshot-server.js +1 -1
- package/dist/services/server-manager.js +5 -5
- package/dist/services/static-report-generator.js +14 -14
- package/dist/services/tdd-service.js +152 -110
- package/dist/services/test-runner.js +3 -3
- package/dist/services/uploader.js +10 -8
- package/dist/types/config.d.ts +2 -1
- package/dist/types/index.d.ts +95 -1
- package/dist/types/sdk.d.ts +1 -1
- package/dist/utils/browser.js +3 -3
- package/dist/utils/build-history.js +12 -12
- package/dist/utils/config-loader.js +17 -17
- package/dist/utils/config-schema.js +6 -6
- package/dist/utils/environment-config.js +11 -0
- package/dist/utils/fetch-utils.js +2 -2
- package/dist/utils/file-helpers.js +2 -2
- package/dist/utils/git.js +3 -6
- package/dist/utils/global-config.js +28 -25
- package/dist/utils/output.js +136 -28
- package/dist/utils/package-info.js +3 -3
- package/dist/utils/security.js +12 -12
- package/docs/api-reference.md +52 -23
- package/docs/plugins.md +60 -25
- package/package.json +9 -13
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* Handles screenshot uploads to the Vizzly platform
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
8
|
+
import { basename } from 'node:path';
|
|
6
9
|
import { glob } from 'glob';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import crypto from 'crypto';
|
|
10
|
+
import { TimeoutError, UploadError, ValidationError } from '../errors/vizzly-error.js';
|
|
11
|
+
import { getDefaultBranch } from '../utils/git.js';
|
|
10
12
|
import * as output from '../utils/output.js';
|
|
11
13
|
import { ApiService } from './api-service.js';
|
|
12
|
-
import { getDefaultBranch } from '../utils/git.js';
|
|
13
|
-
import { UploadError, TimeoutError, ValidationError } from '../errors/vizzly-error.js';
|
|
14
14
|
const DEFAULT_BATCH_SIZE = 50;
|
|
15
15
|
const DEFAULT_SHA_CHECK_BATCH_SIZE = 100;
|
|
16
16
|
const DEFAULT_TIMEOUT = 30000; // 30 seconds
|
|
@@ -25,7 +25,7 @@ export function createUploader({
|
|
|
25
25
|
command,
|
|
26
26
|
upload: uploadConfig = {}
|
|
27
27
|
} = {}, options = {}) {
|
|
28
|
-
|
|
28
|
+
const signal = options.signal || new AbortController().signal;
|
|
29
29
|
const api = new ApiService({
|
|
30
30
|
baseUrl: apiUrl,
|
|
31
31
|
token: apiKey,
|
|
@@ -160,7 +160,7 @@ export function createUploader({
|
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
// Re-throw if already a VizzlyError
|
|
163
|
-
if (error.name
|
|
163
|
+
if (error.name?.includes('Error') && error.code) {
|
|
164
164
|
throw error;
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -304,7 +304,9 @@ async function checkExistingFiles(fileMetadata, api, signal, buildId) {
|
|
|
304
304
|
existing = [],
|
|
305
305
|
screenshots = []
|
|
306
306
|
} = res || {};
|
|
307
|
-
|
|
307
|
+
for (let sha of existing) {
|
|
308
|
+
existingShas.add(sha);
|
|
309
|
+
}
|
|
308
310
|
allScreenshots.push(...screenshots);
|
|
309
311
|
} catch (error) {
|
|
310
312
|
// Continue without deduplication on error
|
package/dist/types/config.d.ts
CHANGED
|
@@ -20,7 +20,8 @@ export { VizzlyConfig } from './index';
|
|
|
20
20
|
* port: 47392
|
|
21
21
|
* },
|
|
22
22
|
* comparison: {
|
|
23
|
-
* threshold: 2.0
|
|
23
|
+
* threshold: 2.0, // CIEDE2000 Delta E (0=exact, 1=JND, 2=recommended)
|
|
24
|
+
* minClusterSize: 2 // Filter single-pixel noise (1=exact, 2=default, 3+=permissive)
|
|
24
25
|
* }
|
|
25
26
|
* });
|
|
26
27
|
*/
|
package/dist/types/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module @vizzly-testing/cli
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { EventEmitter } from 'events';
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
7
7
|
|
|
8
8
|
// ============================================================================
|
|
9
9
|
// Configuration Types
|
|
@@ -29,7 +29,17 @@ export interface UploadConfig {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface ComparisonConfig {
|
|
32
|
+
/** CIEDE2000 Delta E threshold (0=exact, 1=JND, 2=recommended default) */
|
|
32
33
|
threshold?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Minimum cluster size to count as a real difference.
|
|
36
|
+
* Filters out scattered single-pixel noise from rendering variance.
|
|
37
|
+
* - 1 = Exact matching (any different pixel counts)
|
|
38
|
+
* - 2 = Default (filters single isolated pixels as noise)
|
|
39
|
+
* - 3+ = More permissive (only larger clusters detected)
|
|
40
|
+
* @default 2
|
|
41
|
+
*/
|
|
42
|
+
minClusterSize?: number;
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
export interface TddConfig {
|
|
@@ -341,6 +351,90 @@ export interface Services {
|
|
|
341
351
|
testRunner: unknown;
|
|
342
352
|
}
|
|
343
353
|
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// Plugin API Types (Stable Contract)
|
|
356
|
+
// ============================================================================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Stable TestRunner interface for plugins.
|
|
360
|
+
* Only these methods are guaranteed to remain stable across minor versions.
|
|
361
|
+
*/
|
|
362
|
+
export interface PluginTestRunner {
|
|
363
|
+
/** Listen for a single event emission */
|
|
364
|
+
once(event: string, callback: (...args: unknown[]) => void): void;
|
|
365
|
+
/** Subscribe to events */
|
|
366
|
+
on(event: string, callback: (...args: unknown[]) => void): void;
|
|
367
|
+
/** Unsubscribe from events */
|
|
368
|
+
off(event: string, callback: (...args: unknown[]) => void): void;
|
|
369
|
+
/** Create a new build and return the build ID */
|
|
370
|
+
createBuild(options: BuildOptions, isTddMode: boolean): Promise<string>;
|
|
371
|
+
/** Finalize a build after all screenshots are captured */
|
|
372
|
+
finalizeBuild(
|
|
373
|
+
buildId: string,
|
|
374
|
+
isTddMode: boolean,
|
|
375
|
+
success: boolean,
|
|
376
|
+
executionTime: number
|
|
377
|
+
): Promise<void>;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Stable ServerManager interface for plugins.
|
|
382
|
+
* Only these methods are guaranteed to remain stable across minor versions.
|
|
383
|
+
*/
|
|
384
|
+
export interface PluginServerManager {
|
|
385
|
+
/** Start the screenshot server */
|
|
386
|
+
start(buildId: string, tddMode: boolean, setBaseline: boolean): Promise<void>;
|
|
387
|
+
/** Stop the screenshot server */
|
|
388
|
+
stop(): Promise<void>;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Stable services interface for plugins.
|
|
393
|
+
* This is the public API contract - internal services are NOT exposed.
|
|
394
|
+
*/
|
|
395
|
+
export interface PluginServices {
|
|
396
|
+
testRunner: PluginTestRunner;
|
|
397
|
+
serverManager: PluginServerManager;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Build options for createBuild()
|
|
402
|
+
*/
|
|
403
|
+
export interface BuildOptions {
|
|
404
|
+
port?: number;
|
|
405
|
+
timeout?: number;
|
|
406
|
+
buildName?: string;
|
|
407
|
+
branch?: string;
|
|
408
|
+
commit?: string;
|
|
409
|
+
message?: string;
|
|
410
|
+
environment?: string;
|
|
411
|
+
threshold?: number;
|
|
412
|
+
eager?: boolean;
|
|
413
|
+
allowNoToken?: boolean;
|
|
414
|
+
wait?: boolean;
|
|
415
|
+
uploadAll?: boolean;
|
|
416
|
+
pullRequestNumber?: string;
|
|
417
|
+
parallelId?: string;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Context object passed to plugin register() function.
|
|
422
|
+
* This is the stable plugin API contract.
|
|
423
|
+
*/
|
|
424
|
+
export interface PluginContext {
|
|
425
|
+
/** Merged Vizzly configuration */
|
|
426
|
+
config: VizzlyConfig;
|
|
427
|
+
/** Stable services for plugins */
|
|
428
|
+
services: PluginServices;
|
|
429
|
+
/** Output utilities for logging */
|
|
430
|
+
output: OutputUtils;
|
|
431
|
+
/** @deprecated Use output instead. Alias for backwards compatibility. */
|
|
432
|
+
logger: OutputUtils;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Create stable plugin services from internal services */
|
|
436
|
+
export function createPluginServices(services: Services): PluginServices;
|
|
437
|
+
|
|
344
438
|
// ============================================================================
|
|
345
439
|
// Output Utilities
|
|
346
440
|
// ============================================================================
|
package/dist/types/sdk.d.ts
CHANGED
package/dist/utils/browser.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Browser utilities for opening URLs
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { execFile } from 'child_process';
|
|
6
|
-
import { platform } from 'os';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { platform } from 'node:os';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Open a URL in the default browser
|
|
@@ -14,7 +14,7 @@ export async function openBrowser(url) {
|
|
|
14
14
|
return new Promise(resolve => {
|
|
15
15
|
let command;
|
|
16
16
|
let args;
|
|
17
|
-
|
|
17
|
+
const os = platform();
|
|
18
18
|
switch (os) {
|
|
19
19
|
case 'darwin':
|
|
20
20
|
// macOS
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
2
|
-
import { join } from 'path';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Archive a build to history directory
|
|
@@ -11,7 +11,7 @@ import { join } from 'path';
|
|
|
11
11
|
* @param {number} maxHistory - Maximum number of builds to keep (default: 3)
|
|
12
12
|
*/
|
|
13
13
|
export function archiveBuild(workingDir, buildId, builds, comparisons, summary, maxHistory = 3) {
|
|
14
|
-
|
|
14
|
+
const historyDir = join(workingDir, '.vizzly', 'history');
|
|
15
15
|
|
|
16
16
|
// Create history directory if it doesn't exist
|
|
17
17
|
if (!existsSync(historyDir)) {
|
|
@@ -21,13 +21,13 @@ export function archiveBuild(workingDir, buildId, builds, comparisons, summary,
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// Save current build to history
|
|
24
|
-
|
|
24
|
+
const buildDir = join(historyDir, buildId);
|
|
25
25
|
if (!existsSync(buildDir)) {
|
|
26
26
|
mkdirSync(buildDir, {
|
|
27
27
|
recursive: true
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
const buildData = {
|
|
31
31
|
buildId,
|
|
32
32
|
timestamp: Date.now(),
|
|
33
33
|
builds,
|
|
@@ -46,20 +46,20 @@ export function archiveBuild(workingDir, buildId, builds, comparisons, summary,
|
|
|
46
46
|
* @returns {Array} Array of build metadata
|
|
47
47
|
*/
|
|
48
48
|
export function getArchivedBuilds(workingDir) {
|
|
49
|
-
|
|
49
|
+
const historyDir = join(workingDir, '.vizzly', 'history');
|
|
50
50
|
if (!existsSync(historyDir)) {
|
|
51
51
|
return [];
|
|
52
52
|
}
|
|
53
53
|
try {
|
|
54
|
-
|
|
54
|
+
const buildDirs = readdirSync(historyDir, {
|
|
55
55
|
withFileTypes: true
|
|
56
56
|
}).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name).sort().reverse(); // Newest first
|
|
57
57
|
|
|
58
58
|
return buildDirs.map(buildId => {
|
|
59
|
-
|
|
59
|
+
const reportPath = join(historyDir, buildId, 'report.json');
|
|
60
60
|
if (existsSync(reportPath)) {
|
|
61
61
|
try {
|
|
62
|
-
|
|
62
|
+
const data = JSON.parse(require('node:fs').readFileSync(reportPath, 'utf8'));
|
|
63
63
|
return {
|
|
64
64
|
buildId: data.buildId,
|
|
65
65
|
timestamp: data.timestamp,
|
|
@@ -82,15 +82,15 @@ export function getArchivedBuilds(workingDir) {
|
|
|
82
82
|
*/
|
|
83
83
|
function cleanupOldBuilds(historyDir, maxHistory) {
|
|
84
84
|
try {
|
|
85
|
-
|
|
85
|
+
const buildDirs = readdirSync(historyDir, {
|
|
86
86
|
withFileTypes: true
|
|
87
87
|
}).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name).sort().reverse(); // Newest first
|
|
88
88
|
|
|
89
89
|
// Remove builds beyond maxHistory
|
|
90
90
|
if (buildDirs.length > maxHistory) {
|
|
91
|
-
|
|
91
|
+
const toRemove = buildDirs.slice(maxHistory);
|
|
92
92
|
toRemove.forEach(buildId => {
|
|
93
|
-
|
|
93
|
+
const buildDir = join(historyDir, buildId);
|
|
94
94
|
rmSync(buildDir, {
|
|
95
95
|
recursive: true,
|
|
96
96
|
force: true
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
1
2
|
import { cosmiconfigSync } from 'cosmiconfig';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
|
-
import { getApiToken, getApiUrl, getParallelId } from './environment-config.js';
|
|
4
3
|
import { validateVizzlyConfigWithDefaults } from './config-schema.js';
|
|
4
|
+
import { getApiToken, getApiUrl, getParallelId } from './environment-config.js';
|
|
5
5
|
import { getProjectMapping } from './global-config.js';
|
|
6
6
|
import * as output from './output.js';
|
|
7
|
-
|
|
7
|
+
const DEFAULT_CONFIG = {
|
|
8
8
|
// API Configuration
|
|
9
9
|
apiKey: undefined,
|
|
10
10
|
// Will be set from env, global config, or CLI overrides
|
|
@@ -38,19 +38,19 @@ let DEFAULT_CONFIG = {
|
|
|
38
38
|
};
|
|
39
39
|
export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
40
40
|
// 1. Load from config file using cosmiconfig
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const explorer = cosmiconfigSync('vizzly');
|
|
42
|
+
const result = configPath ? explorer.load(configPath) : explorer.search();
|
|
43
43
|
let fileConfig = {};
|
|
44
|
-
if (result
|
|
44
|
+
if (result?.config) {
|
|
45
45
|
// Handle ESM default export (cosmiconfig wraps it in { default: {...} })
|
|
46
46
|
fileConfig = result.config.default || result.config;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// 2. Validate config file using Zod schema
|
|
50
|
-
|
|
50
|
+
const validatedFileConfig = validateVizzlyConfigWithDefaults(fileConfig);
|
|
51
51
|
|
|
52
52
|
// Create a proper clone of the default config to avoid shared object references
|
|
53
|
-
|
|
53
|
+
const config = {
|
|
54
54
|
...DEFAULT_CONFIG,
|
|
55
55
|
server: {
|
|
56
56
|
...DEFAULT_CONFIG.server
|
|
@@ -75,9 +75,9 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
75
75
|
|
|
76
76
|
// 3. Check project mapping for current directory (if no CLI flag)
|
|
77
77
|
if (!cliOverrides.token) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (projectMapping
|
|
78
|
+
const currentDir = process.cwd();
|
|
79
|
+
const projectMapping = await getProjectMapping(currentDir);
|
|
80
|
+
if (projectMapping?.token) {
|
|
81
81
|
// Handle both string tokens and token objects (backward compatibility)
|
|
82
82
|
let token;
|
|
83
83
|
if (typeof projectMapping.token === 'string') {
|
|
@@ -99,9 +99,9 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// 4. Override with environment variables (higher priority than fallbacks)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
const envApiKey = getApiToken();
|
|
103
|
+
const envApiUrl = getApiUrl();
|
|
104
|
+
const envParallelId = getParallelId();
|
|
105
105
|
if (envApiKey) {
|
|
106
106
|
config.apiKey = envApiKey;
|
|
107
107
|
output.debug('Using API token from environment');
|
|
@@ -159,7 +159,7 @@ function applyCLIOverrides(config, cliOverrides = {}) {
|
|
|
159
159
|
if (cliOverrides.allowNoToken !== undefined) config.allowNoToken = cliOverrides.allowNoToken;
|
|
160
160
|
}
|
|
161
161
|
function mergeConfig(target, source) {
|
|
162
|
-
for (
|
|
162
|
+
for (const key in source) {
|
|
163
163
|
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
164
164
|
if (!target[key]) target[key] = {};
|
|
165
165
|
mergeConfig(target[key], source[key]);
|
|
@@ -169,7 +169,7 @@ function mergeConfig(target, source) {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
export function getScreenshotPaths(config) {
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
const screenshotsDir = config.upload?.screenshotsDir || './screenshots';
|
|
173
|
+
const paths = Array.isArray(screenshotsDir) ? screenshotsDir : [screenshotsDir];
|
|
174
174
|
return paths.map(p => resolve(process.cwd(), p));
|
|
175
175
|
}
|
|
@@ -8,7 +8,7 @@ import { z } from 'zod';
|
|
|
8
8
|
/**
|
|
9
9
|
* Server configuration schema
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
const serverSchema = z.object({
|
|
12
12
|
port: z.number().int().positive().default(47392),
|
|
13
13
|
timeout: z.number().int().positive().default(30000)
|
|
14
14
|
});
|
|
@@ -16,7 +16,7 @@ let serverSchema = z.object({
|
|
|
16
16
|
/**
|
|
17
17
|
* Build configuration schema
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
const buildSchema = z.object({
|
|
20
20
|
name: z.string().default('Build {timestamp}'),
|
|
21
21
|
environment: z.string().default('test'),
|
|
22
22
|
branch: z.string().optional(),
|
|
@@ -27,7 +27,7 @@ let buildSchema = z.object({
|
|
|
27
27
|
/**
|
|
28
28
|
* Upload configuration schema
|
|
29
29
|
*/
|
|
30
|
-
|
|
30
|
+
const uploadSchema = z.object({
|
|
31
31
|
screenshotsDir: z.union([z.string(), z.array(z.string())]).default('./screenshots'),
|
|
32
32
|
batchSize: z.number().int().positive().default(10),
|
|
33
33
|
timeout: z.number().int().positive().default(30000)
|
|
@@ -37,14 +37,14 @@ let uploadSchema = z.object({
|
|
|
37
37
|
* Comparison configuration schema
|
|
38
38
|
* threshold: CIEDE2000 Delta E units (0.0 = exact, 1.0 = JND, 2.0 = recommended, 3.0+ = permissive)
|
|
39
39
|
*/
|
|
40
|
-
|
|
40
|
+
const comparisonSchema = z.object({
|
|
41
41
|
threshold: z.number().min(0).default(2.0)
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* TDD configuration schema
|
|
46
46
|
*/
|
|
47
|
-
|
|
47
|
+
const tddSchema = z.object({
|
|
48
48
|
openReport: z.boolean().default(false)
|
|
49
49
|
});
|
|
50
50
|
|
|
@@ -52,7 +52,7 @@ let tddSchema = z.object({
|
|
|
52
52
|
* Core Vizzly configuration schema
|
|
53
53
|
* Allows plugin-specific keys with passthrough for extensibility
|
|
54
54
|
*/
|
|
55
|
-
export
|
|
55
|
+
export const vizzlyConfigSchema = z.object({
|
|
56
56
|
// Core Vizzly config
|
|
57
57
|
apiKey: z.string().optional(),
|
|
58
58
|
apiUrl: z.string().url().optional(),
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
* Centralized access to environment variables with proper defaults
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Get the Vizzly home directory from environment
|
|
8
|
+
* Used to override the default ~/.vizzly directory for storing auth, project mappings, etc.
|
|
9
|
+
* Useful for development (separate dev/prod configs) or testing (isolated test configs)
|
|
10
|
+
* @returns {string|undefined} Custom home directory path
|
|
11
|
+
*/
|
|
12
|
+
export function getVizzlyHome() {
|
|
13
|
+
return process.env.VIZZLY_HOME;
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
/**
|
|
7
17
|
* Get API token from environment
|
|
8
18
|
* @returns {string|undefined} API token
|
|
@@ -89,6 +99,7 @@ export function setVizzlyEnabled(enabled) {
|
|
|
89
99
|
*/
|
|
90
100
|
export function getAllEnvironmentConfig() {
|
|
91
101
|
return {
|
|
102
|
+
home: getVizzlyHome(),
|
|
92
103
|
apiToken: getApiToken(),
|
|
93
104
|
apiUrl: getApiUrl(),
|
|
94
105
|
logLevel: getLogLevel(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
function fetchWithTimeout(url, opts = {}, ms = 300000) {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
const ctrl = new AbortController();
|
|
3
|
+
const id = setTimeout(() => ctrl.abort(), ms);
|
|
4
4
|
return fetch(url, {
|
|
5
5
|
...opts,
|
|
6
6
|
signal: ctrl.signal
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* @description Utilities for handling file-based screenshot inputs
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, readFileSync } from 'fs';
|
|
7
|
-
import { resolve } from 'path';
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
8
|
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
9
9
|
|
|
10
10
|
/**
|
package/dist/utils/git.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { exec } from 'child_process';
|
|
2
|
-
import { promisify } from 'util';
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
3
|
import { getBranch as getCIBranch, getCommit as getCICommit, getCommitMessage as getCICommitMessage, getPullRequestNumber } from './ci-env.js';
|
|
4
4
|
const execAsync = promisify(exec);
|
|
5
5
|
export async function getCommonAncestor(commit1, commit2, cwd = process.cwd()) {
|
|
@@ -55,10 +55,7 @@ async function getCurrentBranchFallback(cwd = process.cwd()) {
|
|
|
55
55
|
cwd
|
|
56
56
|
});
|
|
57
57
|
return branch;
|
|
58
|
-
} catch {
|
|
59
|
-
// Branch doesn't exist, try next one
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
58
|
+
} catch {}
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
// If none of the common branches exist, try to get any local branch
|
|
@@ -3,17 +3,20 @@
|
|
|
3
3
|
* Manages ~/.vizzly/config.json for storing authentication tokens
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { dirname, join, parse } from 'node:path';
|
|
10
10
|
import * as output from './output.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Get the path to the global Vizzly directory
|
|
14
|
-
* @returns {string} Path to ~/.vizzly
|
|
14
|
+
* @returns {string} Path to VIZZLY_HOME or ~/.vizzly
|
|
15
15
|
*/
|
|
16
16
|
export function getGlobalConfigDir() {
|
|
17
|
+
if (process.env.VIZZLY_HOME) {
|
|
18
|
+
return process.env.VIZZLY_HOME;
|
|
19
|
+
}
|
|
17
20
|
return join(homedir(), '.vizzly');
|
|
18
21
|
}
|
|
19
22
|
|
|
@@ -30,7 +33,7 @@ export function getGlobalConfigPath() {
|
|
|
30
33
|
* @returns {Promise<void>}
|
|
31
34
|
*/
|
|
32
35
|
async function ensureGlobalConfigDir() {
|
|
33
|
-
|
|
36
|
+
const dir = getGlobalConfigDir();
|
|
34
37
|
if (!existsSync(dir)) {
|
|
35
38
|
await mkdir(dir, {
|
|
36
39
|
recursive: true,
|
|
@@ -45,11 +48,11 @@ async function ensureGlobalConfigDir() {
|
|
|
45
48
|
*/
|
|
46
49
|
export async function loadGlobalConfig() {
|
|
47
50
|
try {
|
|
48
|
-
|
|
51
|
+
const configPath = getGlobalConfigPath();
|
|
49
52
|
if (!existsSync(configPath)) {
|
|
50
53
|
return {};
|
|
51
54
|
}
|
|
52
|
-
|
|
55
|
+
const content = await readFile(configPath, 'utf-8');
|
|
53
56
|
return JSON.parse(content);
|
|
54
57
|
} catch (error) {
|
|
55
58
|
// If file doesn't exist or is corrupted, return empty config
|
|
@@ -70,8 +73,8 @@ export async function loadGlobalConfig() {
|
|
|
70
73
|
*/
|
|
71
74
|
export async function saveGlobalConfig(config) {
|
|
72
75
|
await ensureGlobalConfigDir();
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
const configPath = getGlobalConfigPath();
|
|
77
|
+
const content = JSON.stringify(config, null, 2);
|
|
75
78
|
|
|
76
79
|
// Write file with secure permissions (owner read/write only)
|
|
77
80
|
await writeFile(configPath, content, {
|
|
@@ -102,7 +105,7 @@ export async function clearGlobalConfig() {
|
|
|
102
105
|
* @returns {Promise<Object|null>} Token object with accessToken, refreshToken, expiresAt, user, or null if not found
|
|
103
106
|
*/
|
|
104
107
|
export async function getAuthTokens() {
|
|
105
|
-
|
|
108
|
+
const config = await loadGlobalConfig();
|
|
106
109
|
if (!config.auth || !config.auth.accessToken) {
|
|
107
110
|
return null;
|
|
108
111
|
}
|
|
@@ -115,7 +118,7 @@ export async function getAuthTokens() {
|
|
|
115
118
|
* @returns {Promise<void>}
|
|
116
119
|
*/
|
|
117
120
|
export async function saveAuthTokens(auth) {
|
|
118
|
-
|
|
121
|
+
const config = await loadGlobalConfig();
|
|
119
122
|
config.auth = {
|
|
120
123
|
accessToken: auth.accessToken,
|
|
121
124
|
refreshToken: auth.refreshToken,
|
|
@@ -130,7 +133,7 @@ export async function saveAuthTokens(auth) {
|
|
|
130
133
|
* @returns {Promise<void>}
|
|
131
134
|
*/
|
|
132
135
|
export async function clearAuthTokens() {
|
|
133
|
-
|
|
136
|
+
const config = await loadGlobalConfig();
|
|
134
137
|
delete config.auth;
|
|
135
138
|
await saveGlobalConfig(config);
|
|
136
139
|
}
|
|
@@ -140,18 +143,18 @@ export async function clearAuthTokens() {
|
|
|
140
143
|
* @returns {Promise<boolean>} True if valid tokens exist
|
|
141
144
|
*/
|
|
142
145
|
export async function hasValidTokens() {
|
|
143
|
-
|
|
146
|
+
const auth = await getAuthTokens();
|
|
144
147
|
if (!auth || !auth.accessToken) {
|
|
145
148
|
return false;
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
// Check if token is expired
|
|
149
152
|
if (auth.expiresAt) {
|
|
150
|
-
|
|
151
|
-
|
|
153
|
+
const expiresAt = new Date(auth.expiresAt);
|
|
154
|
+
const now = new Date();
|
|
152
155
|
|
|
153
156
|
// Consider expired if within 5 minutes of expiry
|
|
154
|
-
|
|
157
|
+
const bufferMs = 5 * 60 * 1000;
|
|
155
158
|
if (now.getTime() >= expiresAt.getTime() - bufferMs) {
|
|
156
159
|
return false;
|
|
157
160
|
}
|
|
@@ -164,7 +167,7 @@ export async function hasValidTokens() {
|
|
|
164
167
|
* @returns {Promise<string|null>} Access token or null
|
|
165
168
|
*/
|
|
166
169
|
export async function getAccessToken() {
|
|
167
|
-
|
|
170
|
+
const auth = await getAuthTokens();
|
|
168
171
|
return auth?.accessToken || null;
|
|
169
172
|
}
|
|
170
173
|
|
|
@@ -175,14 +178,14 @@ export async function getAccessToken() {
|
|
|
175
178
|
* @returns {Promise<Object|null>} Project data or null
|
|
176
179
|
*/
|
|
177
180
|
export async function getProjectMapping(directoryPath) {
|
|
178
|
-
|
|
181
|
+
const config = await loadGlobalConfig();
|
|
179
182
|
if (!config.projects) {
|
|
180
183
|
return null;
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
// Walk up the directory tree looking for a mapping
|
|
184
187
|
let currentPath = directoryPath;
|
|
185
|
-
|
|
188
|
+
const {
|
|
186
189
|
root
|
|
187
190
|
} = parse(currentPath);
|
|
188
191
|
while (currentPath !== root) {
|
|
@@ -191,7 +194,7 @@ export async function getProjectMapping(directoryPath) {
|
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
// Move to parent directory
|
|
194
|
-
|
|
197
|
+
const parentPath = dirname(currentPath);
|
|
195
198
|
if (parentPath === currentPath) {
|
|
196
199
|
// We've reached the root
|
|
197
200
|
break;
|
|
@@ -211,7 +214,7 @@ export async function getProjectMapping(directoryPath) {
|
|
|
211
214
|
* @param {string} projectData.projectName - Project name
|
|
212
215
|
*/
|
|
213
216
|
export async function saveProjectMapping(directoryPath, projectData) {
|
|
214
|
-
|
|
217
|
+
const config = await loadGlobalConfig();
|
|
215
218
|
if (!config.projects) {
|
|
216
219
|
config.projects = {};
|
|
217
220
|
}
|
|
@@ -227,7 +230,7 @@ export async function saveProjectMapping(directoryPath, projectData) {
|
|
|
227
230
|
* @returns {Promise<Object>} Map of directory paths to project data
|
|
228
231
|
*/
|
|
229
232
|
export async function getProjectMappings() {
|
|
230
|
-
|
|
233
|
+
const config = await loadGlobalConfig();
|
|
231
234
|
return config.projects || {};
|
|
232
235
|
}
|
|
233
236
|
|
|
@@ -236,8 +239,8 @@ export async function getProjectMappings() {
|
|
|
236
239
|
* @param {string} directoryPath - Absolute path to project directory
|
|
237
240
|
*/
|
|
238
241
|
export async function deleteProjectMapping(directoryPath) {
|
|
239
|
-
|
|
240
|
-
if (config.projects
|
|
242
|
+
const config = await loadGlobalConfig();
|
|
243
|
+
if (config.projects?.[directoryPath]) {
|
|
241
244
|
delete config.projects[directoryPath];
|
|
242
245
|
await saveGlobalConfig(config);
|
|
243
246
|
}
|