@vizzly-testing/cli 0.20.1-beta.0 → 0.20.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.
Files changed (38) hide show
  1. package/README.md +16 -18
  2. package/dist/cli.js +177 -2
  3. package/dist/client/index.js +144 -77
  4. package/dist/commands/doctor.js +118 -33
  5. package/dist/commands/finalize.js +8 -3
  6. package/dist/commands/init.js +13 -18
  7. package/dist/commands/login.js +42 -49
  8. package/dist/commands/logout.js +13 -5
  9. package/dist/commands/project.js +95 -67
  10. package/dist/commands/run.js +32 -6
  11. package/dist/commands/status.js +81 -50
  12. package/dist/commands/tdd-daemon.js +61 -32
  13. package/dist/commands/tdd.js +14 -26
  14. package/dist/commands/upload.js +18 -9
  15. package/dist/commands/whoami.js +40 -38
  16. package/dist/reporter/reporter-bundle.css +1 -1
  17. package/dist/reporter/reporter-bundle.iife.js +204 -22
  18. package/dist/server/handlers/tdd-handler.js +113 -7
  19. package/dist/server/http-server.js +9 -3
  20. package/dist/server/routers/baseline.js +58 -0
  21. package/dist/server/routers/dashboard.js +10 -6
  22. package/dist/server/routers/screenshot.js +32 -0
  23. package/dist/server-manager/core.js +5 -2
  24. package/dist/server-manager/operations.js +2 -1
  25. package/dist/services/config-service.js +306 -0
  26. package/dist/tdd/tdd-service.js +190 -126
  27. package/dist/types/client.d.ts +25 -2
  28. package/dist/utils/colors.js +187 -39
  29. package/dist/utils/config-loader.js +3 -6
  30. package/dist/utils/context.js +228 -0
  31. package/dist/utils/output.js +449 -14
  32. package/docs/api-reference.md +173 -8
  33. package/docs/tui-elements.md +560 -0
  34. package/package.json +13 -7
  35. package/dist/report-generator/core.js +0 -315
  36. package/dist/report-generator/index.js +0 -8
  37. package/dist/report-generator/operations.js +0 -196
  38. package/dist/services/static-report-generator.js +0 -65
@@ -20,8 +20,14 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
20
20
  // Check if server already running
21
21
  if (await isServerRunning(options.port || 47392)) {
22
22
  const port = options.port || 47392;
23
- output.info(`TDD server already running at http://localhost:${port}`);
24
- output.info(`Dashboard: http://localhost:${port}/dashboard`);
23
+ let colors = output.getColors();
24
+ output.header('tdd', 'local');
25
+ output.print(` ${output.statusDot('success')} Already running`);
26
+ output.blank();
27
+ output.printBox(colors.brand.info(colors.underline(`http://localhost:${port}`)), {
28
+ title: 'Dashboard',
29
+ style: 'branded'
30
+ });
25
31
  if (options.open) {
26
32
  openDashboard(port);
27
33
  }
@@ -37,6 +43,9 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
37
43
  }
38
44
  const port = options.port || 47392;
39
45
 
46
+ // Show header first so debug messages appear below it
47
+ output.header('tdd', 'local');
48
+
40
49
  // Show loading indicator if downloading baselines (but not in verbose mode since child shows progress)
41
50
  if (options.baselineBuild && !globalOptions.verbose) {
42
51
  output.startSpinner(`Downloading baselines from build ${options.baselineBuild}...`);
@@ -109,7 +118,6 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
109
118
  output.error('Failed to start TDD server - server not responding to health checks');
110
119
  process.exit(1);
111
120
  }
112
- output.success(`TDD server started at http://localhost:${port}`);
113
121
 
114
122
  // Write server info to global location for SDK discovery (iOS/Swift can read this)
115
123
  try {
@@ -129,22 +137,29 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
129
137
  } catch {
130
138
  // Non-fatal, SDK can still use health check
131
139
  }
140
+
141
+ // Get colors for styled output
142
+ let colors = output.getColors();
143
+
144
+ // Show dashboard URL in a branded box
145
+ let dashboardUrl = `http://localhost:${port}`;
146
+ output.printBox(colors.brand.info(colors.underline(dashboardUrl)), {
147
+ title: 'Dashboard',
148
+ style: 'branded'
149
+ });
150
+
151
+ // Verbose mode: show next steps
152
+ if (globalOptions.verbose) {
153
+ output.blank();
154
+ output.print(` ${colors.brand.textTertiary('Next steps')}`);
155
+ output.print(` ${colors.brand.textMuted('1.')} Run tests in watch mode ${colors.brand.textMuted('(npm test -- --watch)')}`);
156
+ output.print(` ${colors.brand.textMuted('2.')} Review visual changes in the dashboard`);
157
+ output.print(` ${colors.brand.textMuted('3.')} Accept or reject baseline updates`);
158
+ }
159
+
160
+ // Always show stop hint
132
161
  output.blank();
133
- output.info('Dashboard:');
134
- output.info(` http://localhost:${port}/`);
135
- output.blank();
136
- output.info('Available views:');
137
- output.info(` Comparisons: http://localhost:${port}/`);
138
- output.info(` Stats: http://localhost:${port}/stats`);
139
- output.info(` Settings: http://localhost:${port}/settings`);
140
- output.info(` Projects: http://localhost:${port}/projects`);
141
- output.blank();
142
- output.info('Next steps:');
143
- output.info(' 1. Run your tests in watch mode (e.g., npm test -- --watch)');
144
- output.info(' 2. View live visual comparisons in the dashboard');
145
- output.info(' 3. Accept/reject baselines directly in the UI');
146
- output.blank();
147
- output.info('Stop server: npx vizzly dev stop');
162
+ output.hint('Stop with: vizzly tdd stop');
148
163
  if (options.open) {
149
164
  openDashboard(port);
150
165
  }
@@ -289,9 +304,11 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
289
304
  return;
290
305
  }
291
306
  try {
307
+ let _colors = output.getColors();
308
+
292
309
  // Try to kill the process gracefully
293
310
  process.kill(pid, 'SIGTERM');
294
- output.info(`Stopping TDD server (PID: ${pid})...`);
311
+ output.startSpinner('Stopping TDD server...');
295
312
 
296
313
  // Give it a moment to shut down gracefully
297
314
  await new Promise(resolve => setTimeout(resolve, 2000));
@@ -301,15 +318,16 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
301
318
  process.kill(pid, 0); // Just check if process exists
302
319
  // If we get here, process is still running, force kill it
303
320
  process.kill(pid, 'SIGKILL');
304
- output.info('Force killed TDD server');
321
+ output.stopSpinner();
322
+ output.debug('tdd', 'Force killed process');
305
323
  } catch {
306
324
  // Process is gone, which is what we want
325
+ output.stopSpinner();
307
326
  }
308
327
 
309
328
  // Clean up files
310
329
  if (existsSync(pidFile)) unlinkSync(pidFile);
311
330
  if (existsSync(serverFile)) unlinkSync(serverFile);
312
- output.success('TDD server stopped');
313
331
  } catch (error) {
314
332
  if (error.code === 'ESRCH') {
315
333
  // Process not found - clean up stale files
@@ -356,25 +374,36 @@ export async function tddStatusCommand(_options, globalOptions = {}) {
356
374
  // Try to check health endpoint
357
375
  const health = await checkServerHealth(serverInfo.port);
358
376
  if (health.running) {
359
- output.success(`TDD server running (PID: ${pid})`);
360
- output.info(`Dashboard: http://localhost:${serverInfo.port}/`);
361
- output.blank();
362
- output.info('Available views:');
363
- output.info(` Comparisons: http://localhost:${serverInfo.port}/`);
364
- output.info(` Stats: http://localhost:${serverInfo.port}/stats`);
365
- output.info(` Settings: http://localhost:${serverInfo.port}/settings`);
366
- output.info(` Projects: http://localhost:${serverInfo.port}/projects`);
377
+ let colors = output.getColors();
378
+
379
+ // Show header
380
+ output.header('tdd', 'local');
381
+
382
+ // Show running status with uptime
383
+ let uptimeStr = '';
367
384
  if (serverInfo.startTime) {
368
385
  const uptime = Math.floor((Date.now() - serverInfo.startTime) / 1000);
369
386
  const hours = Math.floor(uptime / 3600);
370
387
  const minutes = Math.floor(uptime % 3600 / 60);
371
388
  const seconds = uptime % 60;
372
- let uptimeStr = '';
373
389
  if (hours > 0) uptimeStr += `${hours}h `;
374
390
  if (minutes > 0 || hours > 0) uptimeStr += `${minutes}m `;
375
391
  uptimeStr += `${seconds}s`;
392
+ }
393
+ output.print(` ${output.statusDot('success')} Running ${uptimeStr ? colors.brand.textTertiary(`· ${uptimeStr}`) : ''}`);
394
+ output.blank();
395
+
396
+ // Show dashboard URL in a branded box
397
+ let dashboardUrl = `http://localhost:${serverInfo.port}`;
398
+ output.printBox(colors.brand.info(colors.underline(dashboardUrl)), {
399
+ title: 'Dashboard',
400
+ style: 'branded'
401
+ });
402
+
403
+ // Verbose mode: show PID
404
+ if (globalOptions.verbose) {
376
405
  output.blank();
377
- output.info(`Uptime: ${uptimeStr}`);
406
+ output.print(` ${colors.brand.textTertiary('PID:')} ${pid}`);
378
407
  }
379
408
  } else {
380
409
  output.warn('TDD server process exists but not responding to health checks');
@@ -430,7 +459,7 @@ async function checkServerHealth(port = 47392) {
430
459
  * @private
431
460
  */
432
461
  function openDashboard(port = 47392) {
433
- const url = `http://localhost:${port}/dashboard`;
462
+ const url = `http://localhost:${port}`;
434
463
 
435
464
  // Cross-platform open command
436
465
  let openCmd;
@@ -8,6 +8,7 @@ import { createBuild as defaultCreateApiBuild, createApiClient as defaultCreateA
8
8
  import { VizzlyError } from '../errors/vizzly-error.js';
9
9
  import { createServerManager as defaultCreateServerManager } from '../server-manager/index.js';
10
10
  import { createBuildObject as defaultCreateBuildObject } from '../services/build-manager.js';
11
+ import { createConfigService as defaultCreateConfigService } from '../services/config-service.js';
11
12
  import { initializeDaemon as defaultInitializeDaemon, runTests as defaultRunTests } from '../test-runner/index.js';
12
13
  import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
13
14
  import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit } from '../utils/git.js';
@@ -30,6 +31,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
30
31
  getBuild = defaultGetBuild,
31
32
  createServerManager = defaultCreateServerManager,
32
33
  createBuildObject = defaultCreateBuildObject,
34
+ createConfigService = defaultCreateConfigService,
33
35
  initializeDaemon = defaultInitializeDaemon,
34
36
  runTests = defaultRunTests,
35
37
  detectBranch = defaultDetectBranch,
@@ -85,11 +87,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
85
87
  output.header('tdd', mode);
86
88
 
87
89
  // Show config in verbose mode
88
- output.debug('config', 'loaded', {
89
- port: config.server.port,
90
- branch,
91
- threshold: config.comparison.threshold
92
- });
90
+ output.debug('config', `port=${config.server.port} threshold=${config.comparison.threshold}`);
93
91
  }
94
92
 
95
93
  // Create functional dependencies
@@ -99,8 +97,15 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
99
97
  verbose: globalOptions.verbose
100
98
  };
101
99
 
100
+ // Create config service for dashboard settings page
101
+ let configService = createConfigService({
102
+ workingDir: process.cwd()
103
+ });
104
+
102
105
  // Create server manager (functional object)
103
- serverManager = createServerManager(configWithVerbose, {});
106
+ serverManager = createServerManager(configWithVerbose, {
107
+ configService
108
+ });
104
109
 
105
110
  // Create build manager (functional object that provides the interface runTests expects)
106
111
  let buildManager = {
@@ -135,7 +140,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
135
140
  createError: (msg, code) => new VizzlyError(msg, code),
136
141
  output,
137
142
  onServerReady: data => {
138
- output.debug('server', `listening on :${data.port}`);
143
+ output.debug('server', `ready on :${data.port}`);
139
144
  }
140
145
  }
141
146
  });
@@ -172,7 +177,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
172
177
  output.debug('build', `created ${data.buildId?.substring(0, 8)}`);
173
178
  },
174
179
  onServerReady: data => {
175
- output.debug('server', `listening on :${data.port}`);
180
+ output.debug('server', `ready on :${data.port}`);
176
181
  },
177
182
  onFinalizeFailed: data => {
178
183
  output.warn(`Failed to finalize build: ${data.error}`);
@@ -180,26 +185,9 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
180
185
  }
181
186
  });
182
187
 
183
- // Show summary
184
- let {
185
- screenshotsCaptured,
186
- comparisons
187
- } = runResult;
188
-
189
188
  // Determine success based on comparison results
189
+ // (Summary is printed by printResults() in tdd-service.js, called from getTddResults)
190
190
  let hasFailures = runResult.failed || runResult.comparisons?.some(c => c.status === 'failed');
191
- if (comparisons && comparisons.length > 0) {
192
- let passed = comparisons.filter(c => c.status === 'passed').length;
193
- let failed = comparisons.filter(c => c.status === 'failed').length;
194
- if (hasFailures) {
195
- output.error(`${failed} visual difference${failed !== 1 ? 's' : ''} detected`);
196
- output.info(`Check .vizzly/diffs/ for diff images`);
197
- } else {
198
- output.result(`${screenshotsCaptured} screenshot${screenshotsCaptured !== 1 ? 's' : ''} · ${passed} passed`);
199
- }
200
- } else {
201
- output.result(`${screenshotsCaptured} screenshot${screenshotsCaptured !== 1 ? 's' : ''}`);
202
- }
203
191
  return {
204
192
  result: {
205
193
  success: !hasFailures,
@@ -173,32 +173,41 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
173
173
  output.warn(`Failed to finalize build: ${error.message}`);
174
174
  }
175
175
  }
176
- output.success('Upload completed successfully');
176
+ output.complete('Upload completed');
177
177
 
178
178
  // Show Vizzly summary
179
179
  if (result.buildId) {
180
- output.info(`🐻 Vizzly: Uploaded ${result.stats.uploaded} of ${result.stats.total} screenshots to build ${result.buildId}`);
180
+ output.blank();
181
+ output.keyValue({
182
+ Uploaded: `${result.stats.uploaded} of ${result.stats.total}`,
183
+ Build: result.buildId
184
+ });
181
185
  // Use API-provided URL or construct proper URL with org/project context
182
186
  let buildUrl = result.url || (await buildUrlConstructor(result.buildId, config.apiUrl, config.apiKey, deps));
183
- output.info(`🔗 Vizzly: View results at ${buildUrl}`);
187
+ output.blank();
188
+ output.labelValue('View', output.link('Results', buildUrl));
184
189
  }
185
190
 
186
191
  // Wait for build completion if requested
187
192
  if (options.wait && result.buildId) {
188
- output.info('Waiting for build completion...');
189
193
  output.startSpinner('Processing comparisons...');
190
- const buildResult = await uploader.waitForBuild(result.buildId);
191
- output.success('Build processing completed');
194
+ let buildResult = await uploader.waitForBuild(result.buildId);
195
+ output.stopSpinner();
196
+ output.complete('Build processing completed');
192
197
 
193
198
  // Show build processing results
199
+ let colors = output.getColors();
194
200
  if (buildResult.failedComparisons > 0) {
195
- output.warn(`${buildResult.failedComparisons} visual comparisons failed`);
201
+ output.blank();
202
+ output.print(` ${colors.brand.danger(buildResult.failedComparisons)} visual comparisons failed`);
196
203
  } else {
197
- output.success(`All ${buildResult.passedComparisons} visual comparisons passed`);
204
+ output.blank();
205
+ output.print(` ${colors.brand.success(buildResult.passedComparisons)} visual comparisons passed`);
198
206
  }
199
207
  // Use API-provided URL or construct proper URL with org/project context
200
208
  let waitBuildUrl = buildResult.url || (await buildUrlConstructor(result.buildId, config.apiUrl, config.apiKey, deps));
201
- output.info(`🔗 Vizzly: View results at ${waitBuildUrl}`);
209
+ output.blank();
210
+ output.labelValue('View', output.link('Results', waitBuildUrl));
202
211
  }
203
212
  output.cleanup();
204
213
  return {
@@ -20,16 +20,17 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
20
20
  });
21
21
  try {
22
22
  // Check if user is logged in
23
- const auth = await getAuthTokens();
23
+ let auth = await getAuthTokens();
24
24
  if (!auth || !auth.accessToken) {
25
25
  if (globalOptions.json) {
26
26
  output.data({
27
27
  authenticated: false
28
28
  });
29
29
  } else {
30
- output.info('You are not logged in');
30
+ output.header('whoami');
31
+ output.print(' Not logged in');
31
32
  output.blank();
32
- output.info('Run "vizzly login" to authenticate');
33
+ output.hint('Run "vizzly login" to authenticate');
33
34
  }
34
35
  output.cleanup();
35
36
  return;
@@ -57,36 +58,39 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
57
58
  }
58
59
 
59
60
  // Human-readable output
60
- output.success('Authenticated');
61
- output.blank();
61
+ output.header('whoami');
62
62
 
63
- // Show user info
63
+ // Show user info using keyValue
64
64
  if (response.user) {
65
- output.info(`User: ${response.user.name || response.user.username}`);
66
- output.info(`Email: ${response.user.email}`);
65
+ let userInfo = {
66
+ User: response.user.name || response.user.username,
67
+ Email: response.user.email
68
+ };
67
69
  if (response.user.username) {
68
- output.info(`Username: ${response.user.username}`);
70
+ userInfo.Username = response.user.username;
69
71
  }
70
72
  if (globalOptions.verbose && response.user.id) {
71
- output.info(`User ID: ${response.user.id}`);
73
+ userInfo['User ID'] = response.user.id;
72
74
  }
75
+ output.keyValue(userInfo);
73
76
  }
74
77
 
75
- // Show organizations
78
+ // Show organizations as a list
76
79
  if (response.organizations && response.organizations.length > 0) {
77
80
  output.blank();
78
- output.info('Organizations:');
79
- for (const org of response.organizations) {
80
- let orgInfo = ` - ${org.name}`;
81
- if (org.slug) {
82
- orgInfo += ` (@${org.slug})`;
83
- }
84
- if (org.role) {
85
- orgInfo += ` [${org.role}]`;
86
- }
87
- output.print(orgInfo);
88
- if (globalOptions.verbose && org.id) {
89
- output.print(` ID: ${org.id}`);
81
+ output.labelValue('Organizations', '');
82
+ let orgItems = response.organizations.map(org => {
83
+ let parts = [org.name];
84
+ if (org.slug) parts.push(`@${org.slug}`);
85
+ if (org.role) parts.push(`[${org.role}]`);
86
+ return parts.join(' ');
87
+ });
88
+ output.list(orgItems);
89
+ if (globalOptions.verbose) {
90
+ for (let org of response.organizations) {
91
+ if (org.id) {
92
+ output.hint(` ${org.name} ID: ${org.id}`);
93
+ }
90
94
  }
91
95
  }
92
96
  }
@@ -94,29 +98,27 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
94
98
  // Show token expiry info
95
99
  if (auth.expiresAt) {
96
100
  output.blank();
97
- const expiresAt = new Date(auth.expiresAt);
98
- const now = new Date();
99
- const msUntilExpiry = expiresAt.getTime() - now.getTime();
100
- const daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
101
- const hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
102
- const minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
101
+ let expiresAt = new Date(auth.expiresAt);
102
+ let now = new Date();
103
+ let msUntilExpiry = expiresAt.getTime() - now.getTime();
104
+ let daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
105
+ let hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
106
+ let minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
103
107
  if (msUntilExpiry <= 0) {
104
108
  output.warn('Token has expired');
105
- output.blank();
106
- output.info('Run "vizzly login" to refresh your authentication');
109
+ output.hint('Run "vizzly login" to refresh your authentication');
107
110
  } else if (daysUntilExpiry > 0) {
108
- output.info(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleDateString()})`);
111
+ output.hint(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleDateString()})`);
109
112
  } else if (hoursUntilExpiry > 0) {
110
- output.info(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleString()})`);
113
+ output.hint(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`);
111
114
  } else if (minutesUntilExpiry > 0) {
112
- output.info(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
115
+ output.hint(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
113
116
  } else {
114
117
  output.warn('Token expires in less than a minute');
115
- output.blank();
116
- output.info('Run "vizzly login" to refresh your authentication');
118
+ output.hint('Run "vizzly login" to refresh your authentication');
117
119
  }
118
120
  if (globalOptions.verbose) {
119
- output.info(`Token expires at: ${expiresAt.toISOString()}`);
121
+ output.hint(`Token expires at: ${expiresAt.toISOString()}`);
120
122
  }
121
123
  }
122
124
  output.cleanup();
@@ -133,7 +135,7 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
133
135
  } else {
134
136
  output.error('Authentication token is invalid or expired', error);
135
137
  output.blank();
136
- output.info('Run "vizzly login" to authenticate again');
138
+ output.hint('Run "vizzly login" to authenticate again');
137
139
  }
138
140
  output.cleanup();
139
141
  process.exit(1);