@vizzly-testing/cli 0.17.0 → 0.18.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.
Files changed (64) hide show
  1. package/dist/cli.js +84 -58
  2. package/dist/client/index.js +6 -6
  3. package/dist/commands/doctor.js +15 -15
  4. package/dist/commands/finalize.js +7 -7
  5. package/dist/commands/init.js +28 -28
  6. package/dist/commands/login.js +23 -23
  7. package/dist/commands/logout.js +4 -4
  8. package/dist/commands/project.js +36 -36
  9. package/dist/commands/run.js +33 -33
  10. package/dist/commands/status.js +14 -14
  11. package/dist/commands/tdd-daemon.js +43 -43
  12. package/dist/commands/tdd.js +26 -26
  13. package/dist/commands/upload.js +32 -32
  14. package/dist/commands/whoami.js +12 -12
  15. package/dist/index.js +9 -14
  16. package/dist/plugin-loader.js +28 -28
  17. package/dist/reporter/reporter-bundle.css +1 -1
  18. package/dist/reporter/reporter-bundle.iife.js +19 -19
  19. package/dist/sdk/index.js +33 -35
  20. package/dist/server/handlers/api-handler.js +4 -4
  21. package/dist/server/handlers/tdd-handler.js +11 -11
  22. package/dist/server/http-server.js +21 -22
  23. package/dist/server/middleware/json-parser.js +1 -1
  24. package/dist/server/routers/assets.js +14 -14
  25. package/dist/server/routers/auth.js +14 -14
  26. package/dist/server/routers/baseline.js +8 -8
  27. package/dist/server/routers/cloud-proxy.js +15 -15
  28. package/dist/server/routers/config.js +11 -11
  29. package/dist/server/routers/dashboard.js +11 -11
  30. package/dist/server/routers/health.js +4 -4
  31. package/dist/server/routers/projects.js +19 -19
  32. package/dist/server/routers/screenshot.js +9 -9
  33. package/dist/services/api-service.js +16 -16
  34. package/dist/services/auth-service.js +17 -17
  35. package/dist/services/build-manager.js +3 -3
  36. package/dist/services/config-service.js +32 -32
  37. package/dist/services/html-report-generator.js +8 -8
  38. package/dist/services/index.js +11 -11
  39. package/dist/services/project-service.js +19 -19
  40. package/dist/services/report-generator/report.css +3 -3
  41. package/dist/services/report-generator/viewer.js +25 -23
  42. package/dist/services/screenshot-server.js +1 -1
  43. package/dist/services/server-manager.js +5 -5
  44. package/dist/services/static-report-generator.js +14 -14
  45. package/dist/services/tdd-service.js +98 -92
  46. package/dist/services/test-runner.js +3 -3
  47. package/dist/services/uploader.js +10 -8
  48. package/dist/types/config.d.ts +2 -1
  49. package/dist/types/index.d.ts +11 -1
  50. package/dist/types/sdk.d.ts +1 -1
  51. package/dist/utils/browser.js +3 -3
  52. package/dist/utils/build-history.js +12 -12
  53. package/dist/utils/config-loader.js +17 -17
  54. package/dist/utils/config-schema.js +6 -6
  55. package/dist/utils/environment-config.js +11 -0
  56. package/dist/utils/fetch-utils.js +2 -2
  57. package/dist/utils/file-helpers.js +2 -2
  58. package/dist/utils/git.js +3 -6
  59. package/dist/utils/global-config.js +28 -25
  60. package/dist/utils/output.js +136 -28
  61. package/dist/utils/package-info.js +3 -3
  62. package/dist/utils/security.js +12 -12
  63. package/docs/api-reference.md +52 -23
  64. package/package.json +9 -13
@@ -1,5 +1,5 @@
1
- import { existsSync, mkdirSync, writeFileSync, readdirSync, rmSync } from 'fs';
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
- let historyDir = join(workingDir, '.vizzly', 'history');
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
- let buildDir = join(historyDir, buildId);
24
+ const buildDir = join(historyDir, buildId);
25
25
  if (!existsSync(buildDir)) {
26
26
  mkdirSync(buildDir, {
27
27
  recursive: true
28
28
  });
29
29
  }
30
- let buildData = {
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
- let historyDir = join(workingDir, '.vizzly', 'history');
49
+ const historyDir = join(workingDir, '.vizzly', 'history');
50
50
  if (!existsSync(historyDir)) {
51
51
  return [];
52
52
  }
53
53
  try {
54
- let buildDirs = readdirSync(historyDir, {
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
- let reportPath = join(historyDir, buildId, 'report.json');
59
+ const reportPath = join(historyDir, buildId, 'report.json');
60
60
  if (existsSync(reportPath)) {
61
61
  try {
62
- let data = JSON.parse(require('fs').readFileSync(reportPath, 'utf8'));
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
- let buildDirs = readdirSync(historyDir, {
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
- let toRemove = buildDirs.slice(maxHistory);
91
+ const toRemove = buildDirs.slice(maxHistory);
92
92
  toRemove.forEach(buildId => {
93
- let buildDir = join(historyDir, buildId);
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
- let DEFAULT_CONFIG = {
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
- let explorer = cosmiconfigSync('vizzly');
42
- let result = configPath ? explorer.load(configPath) : explorer.search();
41
+ const explorer = cosmiconfigSync('vizzly');
42
+ const result = configPath ? explorer.load(configPath) : explorer.search();
43
43
  let fileConfig = {};
44
- if (result && result.config) {
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
- let validatedFileConfig = validateVizzlyConfigWithDefaults(fileConfig);
50
+ const validatedFileConfig = validateVizzlyConfigWithDefaults(fileConfig);
51
51
 
52
52
  // Create a proper clone of the default config to avoid shared object references
53
- let config = {
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
- let currentDir = process.cwd();
79
- let projectMapping = await getProjectMapping(currentDir);
80
- if (projectMapping && projectMapping.token) {
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
- let envApiKey = getApiToken();
103
- let envApiUrl = getApiUrl();
104
- let envParallelId = getParallelId();
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 (let key in source) {
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
- let screenshotsDir = config.upload?.screenshotsDir || './screenshots';
173
- let paths = Array.isArray(screenshotsDir) ? screenshotsDir : [screenshotsDir];
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
- let serverSchema = z.object({
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
- let buildSchema = z.object({
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
- let uploadSchema = z.object({
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
- let comparisonSchema = z.object({
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
- let tddSchema = z.object({
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 let vizzlyConfigSchema = z.object({
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
- let ctrl = new AbortController();
3
- let id = setTimeout(() => ctrl.abort(), ms);
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 { homedir } from 'os';
7
- import { join, dirname, parse } from 'path';
8
- import { readFile, writeFile, mkdir, chmod } from 'fs/promises';
9
- import { existsSync } from 'fs';
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
- let dir = getGlobalConfigDir();
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
- let configPath = getGlobalConfigPath();
51
+ const configPath = getGlobalConfigPath();
49
52
  if (!existsSync(configPath)) {
50
53
  return {};
51
54
  }
52
- let content = await readFile(configPath, 'utf-8');
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
- let configPath = getGlobalConfigPath();
74
- let content = JSON.stringify(config, null, 2);
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
- let config = await loadGlobalConfig();
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
- let config = await loadGlobalConfig();
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
- let config = await loadGlobalConfig();
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
- let auth = await getAuthTokens();
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
- let expiresAt = new Date(auth.expiresAt);
151
- let now = new Date();
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
- let bufferMs = 5 * 60 * 1000;
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
- let auth = await getAuthTokens();
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
- let config = await loadGlobalConfig();
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
- let {
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
- let parentPath = dirname(currentPath);
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
- let config = await loadGlobalConfig();
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
- let config = await loadGlobalConfig();
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
- let config = await loadGlobalConfig();
240
- if (config.projects && config.projects[directoryPath]) {
242
+ const config = await loadGlobalConfig();
243
+ if (config.projects?.[directoryPath]) {
241
244
  delete config.projects[directoryPath];
242
245
  await saveGlobalConfig(config);
243
246
  }