@vizzly-testing/cli 0.25.1 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -11,7 +11,7 @@ import { projectListCommand, projectRemoveCommand, projectSelectCommand, project
11
11
  import { runCommand, validateRunOptions } from './commands/run.js';
12
12
  import { statusCommand, validateStatusOptions } from './commands/status.js';
13
13
  import { tddCommand, validateTddOptions } from './commands/tdd.js';
14
- import { runDaemonChild, tddStartCommand, tddStatusCommand, tddStopCommand } from './commands/tdd-daemon.js';
14
+ import { runDaemonChild, tddListCommand, tddStartCommand, tddStatusCommand, tddStopCommand } from './commands/tdd-daemon.js';
15
15
  import { uploadCommand, validateUploadOptions } from './commands/upload.js';
16
16
  import { validateWhoamiOptions, whoamiCommand } from './commands/whoami.js';
17
17
  import { createPluginServices } from './plugin-api.js';
@@ -22,6 +22,7 @@ import { openBrowser } from './utils/browser.js';
22
22
  import { colors } from './utils/colors.js';
23
23
  import { loadConfig } from './utils/config-loader.js';
24
24
  import { getContext } from './utils/context.js';
25
+ import { saveUserPath } from './utils/global-config.js';
25
26
  import * as output from './utils/output.js';
26
27
  import { getPackageVersion } from './utils/package-info.js';
27
28
 
@@ -300,6 +301,12 @@ tddCmd.command('status').description('Check TDD server status').action(async opt
300
301
  await tddStatusCommand(options, globalOptions);
301
302
  });
302
303
 
304
+ // TDD List - List all running servers (for menubar app integration)
305
+ tddCmd.command('list').description('List all running TDD servers').action(async options => {
306
+ const globalOptions = program.opts();
307
+ await tddListCommand(options, globalOptions);
308
+ });
309
+
303
310
  // TDD Run - One-off test run with ephemeral server (generates static report)
304
311
  tddCmd.command('run <command>').description('Run tests once in TDD mode with local visual comparisons').option('--port <port>', 'Port for TDD server', '47392').option('--branch <branch>', 'Git branch override').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--token <token>', 'API token override').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--set-baseline', 'Accept current screenshots as new baseline (overwrites existing)').option('--fail-on-diff', 'Fail tests when visual differences are detected').option('--no-open', 'Skip opening report in browser').action(async (command, options) => {
305
312
  const globalOptions = program.opts();
@@ -521,4 +528,8 @@ program.command('project:remove').description('Remove project configuration for
521
528
  const globalOptions = program.opts();
522
529
  await projectRemoveCommand(options, globalOptions);
523
530
  });
531
+
532
+ // Save user's PATH for menubar app (non-blocking, runs in background)
533
+ // This auto-configures the menubar app so it can find npx/node
534
+ saveUserPath().catch(() => {});
524
535
  program.parse();
@@ -1,7 +1,8 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { basename, join } from 'node:path';
5
+ import { getServerRegistry } from '../tdd/server-registry.js';
5
6
  import * as output from '../utils/output.js';
6
7
  import { tddCommand } from './tdd.js';
7
8
 
@@ -16,32 +17,72 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
16
17
  verbose: globalOptions.verbose,
17
18
  color: !globalOptions.noColor
18
19
  });
20
+ let registry = getServerRegistry();
21
+ let colors = output.getColors();
19
22
 
20
- // Check if server already running
21
- if (await isServerRunning(options.port || 47392)) {
22
- const port = options.port || 47392;
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
- });
31
- if (options.open) {
32
- openDashboard(port);
23
+ // Check if THIS directory already has a server running
24
+ let existingServer = registry.find({
25
+ directory: process.cwd()
26
+ });
27
+ if (existingServer) {
28
+ // Verify it's actually running
29
+ if (await isServerRunning(existingServer.port)) {
30
+ output.header('tdd', 'local');
31
+ output.print(` ${output.statusDot('success')} Already running`);
32
+ output.blank();
33
+ output.printBox(colors.brand.info(colors.underline(`http://localhost:${existingServer.port}`)), {
34
+ title: 'Dashboard',
35
+ style: 'branded'
36
+ });
37
+ if (options.open) {
38
+ openDashboard(existingServer.port);
39
+ }
40
+ return;
41
+ } else {
42
+ // Stale entry - clean it up (registry and local files)
43
+ registry.unregister({
44
+ directory: process.cwd()
45
+ });
46
+ let vizzlyDir = join(process.cwd(), '.vizzly');
47
+ let pidFile = join(vizzlyDir, 'server.pid');
48
+ let serverFile = join(vizzlyDir, 'server.json');
49
+ if (existsSync(pidFile)) unlinkSync(pidFile);
50
+ if (existsSync(serverFile)) unlinkSync(serverFile);
33
51
  }
34
- return;
52
+ }
53
+
54
+ // Determine port: user-specified or auto-allocate
55
+ let port;
56
+ let autoAllocated = false;
57
+ if (options.port) {
58
+ // User specified a port - use it (will fail if busy)
59
+ port = options.port;
60
+
61
+ // Check if user-specified port is in use
62
+ if (await isServerRunning(port)) {
63
+ output.header('tdd', 'local');
64
+ output.print(` ${output.statusDot('error')} Port ${port} is already in use`);
65
+ output.blank();
66
+ output.hint('Try a different port: vizzly tdd start --port 47393');
67
+ output.hint('Or let Vizzly auto-allocate: vizzly tdd start');
68
+ return;
69
+ }
70
+ } else {
71
+ // Auto-allocate an available port
72
+ // Note: There's a small race window between finding a port and binding.
73
+ // The registry acts as a soft reservation, and findAvailablePort does
74
+ // an actual TCP bind test to minimize this window.
75
+ port = await registry.findAvailablePort();
76
+ autoAllocated = port !== 47392;
35
77
  }
36
78
  try {
37
79
  // Ensure .vizzly directory exists
38
- const vizzlyDir = join(process.cwd(), '.vizzly');
80
+ let vizzlyDir = join(process.cwd(), '.vizzly');
39
81
  if (!existsSync(vizzlyDir)) {
40
82
  mkdirSync(vizzlyDir, {
41
83
  recursive: true
42
84
  });
43
85
  }
44
- const port = options.port || 47392;
45
86
 
46
87
  // Show header first so debug messages appear below it
47
88
  output.header('tdd', 'local');
@@ -119,7 +160,28 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
119
160
  process.exit(1);
120
161
  }
121
162
 
122
- // Write server info to global location for SDK discovery (iOS/Swift can read this)
163
+ // Register server in global registry (for menubar app)
164
+ try {
165
+ let registry = getServerRegistry();
166
+
167
+ // Clean up any stale servers first
168
+ registry.cleanupStale();
169
+
170
+ // Register this server with log file path for menubar to read
171
+ let serverLogFile = join(process.cwd(), '.vizzly', 'server.log');
172
+ registry.register({
173
+ pid: child.pid,
174
+ port: port,
175
+ directory: process.cwd(),
176
+ name: basename(process.cwd()),
177
+ startedAt: new Date().toISOString(),
178
+ logFile: serverLogFile
179
+ });
180
+ } catch {
181
+ // Non-fatal
182
+ }
183
+
184
+ // Also write legacy server.json for SDK discovery (backwards compatibility)
123
185
  try {
124
186
  const globalVizzlyDir = join(homedir(), '.vizzly');
125
187
  if (!existsSync(globalVizzlyDir)) {
@@ -138,8 +200,11 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
138
200
  // Non-fatal, SDK can still use health check
139
201
  }
140
202
 
141
- // Get colors for styled output
142
- let colors = output.getColors();
203
+ // Show auto-allocated port message if applicable
204
+ if (autoAllocated) {
205
+ output.print(` ${output.statusDot('info')} Auto-assigned port ${colors.brand.textTertiary(`:${port}`)}`);
206
+ output.blank();
207
+ }
143
208
 
144
209
  // Show dashboard URL in a branded box
145
210
  let dashboardUrl = `http://localhost:${port}`;
@@ -177,6 +242,17 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
177
242
  export async function runDaemonChild(options = {}, globalOptions = {}) {
178
243
  const vizzlyDir = join(process.cwd(), '.vizzly');
179
244
  const port = options.port || 47392;
245
+
246
+ // Set up log file for menubar app to read
247
+ const logFile = join(vizzlyDir, 'server.log');
248
+
249
+ // Configure output to write JSON logs to file (before tddCommand configures it)
250
+ output.configure({
251
+ logFile,
252
+ json: globalOptions.json,
253
+ verbose: globalOptions.verbose,
254
+ color: !globalOptions.noColor
255
+ });
180
256
  try {
181
257
  // Use existing tddCommand but with daemon mode
182
258
  const {
@@ -200,7 +276,8 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
200
276
  pid: process.pid,
201
277
  port: port,
202
278
  startTime: Date.now(),
203
- failOnDiff: options.failOnDiff || false
279
+ failOnDiff: options.failOnDiff || false,
280
+ logFile: logFile
204
281
  };
205
282
  writeFileSync(join(vizzlyDir, 'server.json'), JSON.stringify(serverInfo, null, 2));
206
283
 
@@ -212,7 +289,18 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
212
289
  const serverFile = join(vizzlyDir, 'server.json');
213
290
  if (existsSync(serverFile)) unlinkSync(serverFile);
214
291
 
215
- // Clean up global server file
292
+ // Unregister from global registry (for menubar app)
293
+ try {
294
+ let registry = getServerRegistry();
295
+ registry.unregister({
296
+ port: port,
297
+ directory: process.cwd()
298
+ });
299
+ } catch {
300
+ // Non-fatal
301
+ }
302
+
303
+ // Clean up legacy global server file
216
304
  try {
217
305
  const globalServerFile = join(homedir(), '.vizzly', 'server.json');
218
306
  if (existsSync(globalServerFile)) unlinkSync(globalServerFile);
@@ -329,12 +417,35 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
329
417
  // Clean up files
330
418
  if (existsSync(pidFile)) unlinkSync(pidFile);
331
419
  if (existsSync(serverFile)) unlinkSync(serverFile);
420
+
421
+ // Unregister from global registry (for menubar app)
422
+ try {
423
+ let registry = getServerRegistry();
424
+ registry.unregister({
425
+ port: port,
426
+ directory: process.cwd()
427
+ });
428
+ } catch {
429
+ // Non-fatal
430
+ }
431
+ output.print(` ${output.statusDot('success')} Server stopped`);
332
432
  } catch (error) {
333
433
  if (error.code === 'ESRCH') {
334
434
  // Process not found - clean up stale files
335
435
  output.warn('TDD server was not running (cleaning up stale files)');
336
436
  if (existsSync(pidFile)) unlinkSync(pidFile);
337
437
  if (existsSync(serverFile)) unlinkSync(serverFile);
438
+
439
+ // Still unregister from registry
440
+ try {
441
+ let registry = getServerRegistry();
442
+ registry.unregister({
443
+ port: port,
444
+ directory: process.cwd()
445
+ });
446
+ } catch {
447
+ // Non-fatal
448
+ }
338
449
  } else {
339
450
  output.error('Error stopping TDD server', error);
340
451
  }
@@ -475,4 +586,66 @@ function openDashboard(port = 47392) {
475
586
  detached: true,
476
587
  stdio: 'ignore'
477
588
  }).unref();
589
+ }
590
+
591
+ /**
592
+ * List all running TDD servers from the global registry
593
+ * @param {Object} options - Command options
594
+ * @param {Object} globalOptions - Global CLI options
595
+ */
596
+ export async function tddListCommand(_options, globalOptions = {}) {
597
+ output.configure({
598
+ json: globalOptions.json,
599
+ verbose: globalOptions.verbose,
600
+ color: !globalOptions.noColor
601
+ });
602
+ let registry = getServerRegistry();
603
+
604
+ // Clean up stale servers first
605
+ let cleaned = registry.cleanupStale();
606
+ if (cleaned > 0 && globalOptions.verbose) {
607
+ output.debug('tdd', `Cleaned up ${cleaned} stale server(s)`);
608
+ }
609
+ let servers = registry.list();
610
+
611
+ // JSON output
612
+ if (globalOptions.json) {
613
+ console.log(JSON.stringify({
614
+ servers
615
+ }, null, 2));
616
+ return;
617
+ }
618
+
619
+ // No servers
620
+ if (servers.length === 0) {
621
+ output.info('No TDD servers running');
622
+ output.hint('Start one with: vizzly tdd start');
623
+ return;
624
+ }
625
+
626
+ // Table output
627
+ let colors = output.getColors();
628
+ output.header('tdd', 'servers');
629
+ output.blank();
630
+ for (let server of servers) {
631
+ let uptimeStr = '';
632
+ if (server.startedAt) {
633
+ let startTime = new Date(server.startedAt).getTime();
634
+ let uptime = Math.floor((Date.now() - startTime) / 1000);
635
+ let hours = Math.floor(uptime / 3600);
636
+ let minutes = Math.floor(uptime % 3600 / 60);
637
+ if (hours > 0) uptimeStr += `${hours}h `;
638
+ if (minutes > 0 || hours > 0) uptimeStr += `${minutes}m`;else uptimeStr = '<1m';
639
+ }
640
+ let name = server.name || basename(server.directory);
641
+ let portStr = colors.brand.textTertiary(`:${server.port}`);
642
+ let uptimeLabel = uptimeStr ? colors.brand.textMuted(` · ${uptimeStr}`) : '';
643
+ output.print(` ${output.statusDot('success')} ${name}${portStr}${uptimeLabel}`);
644
+ output.print(` ${colors.brand.textMuted(server.directory)}`);
645
+ if (globalOptions.verbose) {
646
+ output.print(` ${colors.brand.textMuted(`PID: ${server.pid}`)}`);
647
+ }
648
+ output.blank();
649
+ }
650
+ output.print(` ${colors.brand.textTertiary(`${servers.length} server(s) running`)}`);
478
651
  }
@@ -0,0 +1,252 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { createServer } from 'node:net';
5
+ import { homedir } from 'node:os';
6
+ import { join } from 'node:path';
7
+
8
+ /**
9
+ * Manages a global registry of running TDD servers at ~/.vizzly/servers.json
10
+ * Enables the menubar app to discover and manage multiple concurrent servers.
11
+ */
12
+ export class ServerRegistry {
13
+ constructor() {
14
+ this.vizzlyHome = process.env.VIZZLY_HOME || join(homedir(), '.vizzly');
15
+ this.registryPath = join(this.vizzlyHome, 'servers.json');
16
+ }
17
+
18
+ /**
19
+ * Ensure the registry directory exists
20
+ */
21
+ ensureDirectory() {
22
+ if (!existsSync(this.vizzlyHome)) {
23
+ mkdirSync(this.vizzlyHome, {
24
+ recursive: true
25
+ });
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Read the current registry, returning empty if it doesn't exist
31
+ */
32
+ read() {
33
+ try {
34
+ if (existsSync(this.registryPath)) {
35
+ let data = JSON.parse(readFileSync(this.registryPath, 'utf8'));
36
+ return {
37
+ version: data.version || 1,
38
+ servers: data.servers || []
39
+ };
40
+ }
41
+ } catch (_err) {
42
+ // Corrupted file, start fresh
43
+ console.warn('Warning: Could not read server registry, starting fresh');
44
+ }
45
+ return {
46
+ version: 1,
47
+ servers: []
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Write the registry to disk
53
+ */
54
+ write(registry) {
55
+ this.ensureDirectory();
56
+ writeFileSync(this.registryPath, JSON.stringify(registry, null, 2));
57
+ }
58
+
59
+ /**
60
+ * Register a new server in the registry
61
+ */
62
+ register(serverInfo) {
63
+ // Validate required fields
64
+ if (!serverInfo.pid || !serverInfo.port || !serverInfo.directory) {
65
+ throw new Error('Missing required fields: pid, port, directory');
66
+ }
67
+ let port = Number(serverInfo.port);
68
+ let pid = Number(serverInfo.pid);
69
+ if (Number.isNaN(port) || Number.isNaN(pid)) {
70
+ throw new Error('Invalid port or pid - must be numbers');
71
+ }
72
+ let registry = this.read();
73
+
74
+ // Remove any existing entry for this port or directory (shouldn't happen, but be safe)
75
+ registry.servers = registry.servers.filter(s => s.port !== port && s.directory !== serverInfo.directory);
76
+
77
+ // Add the new server
78
+ registry.servers.push({
79
+ id: serverInfo.id || randomBytes(8).toString('hex'),
80
+ port,
81
+ pid,
82
+ directory: serverInfo.directory,
83
+ startedAt: serverInfo.startedAt || new Date().toISOString(),
84
+ configPath: serverInfo.configPath || null,
85
+ name: serverInfo.name || null,
86
+ logFile: serverInfo.logFile || null
87
+ });
88
+ this.write(registry);
89
+ this.notifyMenubar();
90
+ return registry;
91
+ }
92
+
93
+ /**
94
+ * Unregister a server by port and/or directory
95
+ * When both are provided, matches servers with BOTH criteria (AND logic)
96
+ * When only one is provided, matches servers with that criteria
97
+ */
98
+ unregister({
99
+ port,
100
+ directory
101
+ }) {
102
+ let registry = this.read();
103
+ let initialCount = registry.servers.length;
104
+ if (port && directory) {
105
+ // Both specified - match servers with both port AND directory
106
+ registry.servers = registry.servers.filter(s => !(s.port === port && s.directory === directory));
107
+ } else if (port) {
108
+ registry.servers = registry.servers.filter(s => s.port !== port);
109
+ } else if (directory) {
110
+ registry.servers = registry.servers.filter(s => s.directory !== directory);
111
+ }
112
+ if (registry.servers.length !== initialCount) {
113
+ this.write(registry);
114
+ this.notifyMenubar();
115
+ }
116
+ return registry;
117
+ }
118
+
119
+ /**
120
+ * Find a server by port or directory
121
+ */
122
+ find({
123
+ port,
124
+ directory
125
+ }) {
126
+ let registry = this.read();
127
+ if (port) {
128
+ return registry.servers.find(s => s.port === port);
129
+ }
130
+ if (directory) {
131
+ return registry.servers.find(s => s.directory === directory);
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Get all registered servers
138
+ */
139
+ list() {
140
+ return this.read().servers;
141
+ }
142
+
143
+ /**
144
+ * Remove servers whose PIDs no longer exist (stale entries)
145
+ */
146
+ cleanupStale() {
147
+ let registry = this.read();
148
+ let initialCount = registry.servers.length;
149
+ registry.servers = registry.servers.filter(server => {
150
+ try {
151
+ // Signal 0 doesn't kill, just checks if process exists
152
+ process.kill(server.pid, 0);
153
+ return true;
154
+ } catch (err) {
155
+ // ESRCH = process doesn't exist, EPERM = exists but no permission (still valid)
156
+ return err.code === 'EPERM';
157
+ }
158
+ });
159
+ if (registry.servers.length !== initialCount) {
160
+ this.write(registry);
161
+ this.notifyMenubar();
162
+ return initialCount - registry.servers.length;
163
+ }
164
+ return 0;
165
+ }
166
+
167
+ /**
168
+ * Notify the menubar app that the registry changed
169
+ *
170
+ * Uses macOS notifyutil for instant Darwin notification delivery.
171
+ * The menubar app listens for this in addition to file watching.
172
+ */
173
+ notifyMenubar() {
174
+ if (process.platform !== 'darwin') return;
175
+ try {
176
+ execSync('notifyutil -p dev.vizzly.serverChanged', {
177
+ stdio: 'ignore',
178
+ timeout: 500
179
+ });
180
+ } catch {
181
+ // Non-fatal - menubar will still see changes via file watching
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Get all ports currently in use by registered servers
187
+ * @returns {Set<number>} Set of ports in use
188
+ */
189
+ getUsedPorts() {
190
+ let registry = this.read();
191
+ return new Set(registry.servers.map(s => s.port));
192
+ }
193
+
194
+ /**
195
+ * Find an available port starting from the default
196
+ * @param {number} startPort - Port to start searching from (default: 47392)
197
+ * @param {number} maxAttempts - Maximum ports to try (default: 100)
198
+ * @returns {Promise<number>} Available port
199
+ */
200
+ async findAvailablePort(startPort = 47392, maxAttempts = 100) {
201
+ // Clean up stale entries first
202
+ this.cleanupStale();
203
+ let usedPorts = this.getUsedPorts();
204
+ for (let i = 0; i < maxAttempts; i++) {
205
+ let port = startPort + i;
206
+
207
+ // Skip if registered in our registry
208
+ if (usedPorts.has(port)) continue;
209
+
210
+ // Check if port is actually free (not used by other apps)
211
+ let isFree = await isPortFree(port);
212
+ if (isFree) {
213
+ return port;
214
+ }
215
+ }
216
+
217
+ // Fallback to default if nothing found (will fail later with clear error)
218
+ return startPort;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Check if a port is free (not in use by any process)
224
+ * @param {number} port - Port to check
225
+ * @returns {Promise<boolean>} True if port is free
226
+ */
227
+ async function isPortFree(port) {
228
+ return new Promise(resolve => {
229
+ let server = createServer();
230
+ server.once('error', err => {
231
+ if (err.code === 'EADDRINUSE') {
232
+ resolve(false);
233
+ } else {
234
+ // Other errors - assume port is free
235
+ resolve(true);
236
+ }
237
+ });
238
+ server.once('listening', () => {
239
+ server.close(() => resolve(true));
240
+ });
241
+ server.listen(port, '127.0.0.1');
242
+ });
243
+ }
244
+
245
+ // Singleton instance
246
+ let registryInstance = null;
247
+ export function getServerRegistry() {
248
+ if (!registryInstance) {
249
+ registryInstance = new ServerRegistry();
250
+ }
251
+ return registryInstance;
252
+ }
@@ -100,6 +100,32 @@ export async function clearGlobalConfig() {
100
100
  await saveGlobalConfig({});
101
101
  }
102
102
 
103
+ /**
104
+ * Save user's PATH for menubar app to use
105
+ * This auto-configures the menubar app so it can find npx/node
106
+ * @returns {Promise<void>}
107
+ */
108
+ export async function saveUserPath() {
109
+ let config = await loadGlobalConfig();
110
+ let userPath = process.env.PATH;
111
+
112
+ // Only update if PATH has changed
113
+ if (config.userPath === userPath) {
114
+ return;
115
+ }
116
+ config.userPath = userPath;
117
+ await saveGlobalConfig(config);
118
+ }
119
+
120
+ /**
121
+ * Get stored user PATH for external tools (like menubar app)
122
+ * @returns {Promise<string|null>} PATH string or null if not configured
123
+ */
124
+ export async function getUserPath() {
125
+ let config = await loadGlobalConfig();
126
+ return config.userPath || null;
127
+ }
128
+
103
129
  /**
104
130
  * Get authentication tokens from global config
105
131
  * @returns {Promise<Object|null>} Token object with accessToken, refreshToken, expiresAt, user, or null if not found
@@ -185,6 +185,10 @@ export function getColors() {
185
185
  */
186
186
  export function success(message, data = {}) {
187
187
  stopSpinner();
188
+ writeLog('info', message, {
189
+ status: 'success',
190
+ ...data
191
+ });
188
192
  if (config.silent) return;
189
193
  if (config.json) {
190
194
  console.log(JSON.stringify({
@@ -221,6 +225,7 @@ export function result(message) {
221
225
  * Show an info message
222
226
  */
223
227
  export function info(message, data = {}) {
228
+ writeLog('info', message, data);
224
229
  if (!shouldLog('info')) return;
225
230
  if (config.json) {
226
231
  console.log(JSON.stringify({
@@ -238,6 +243,7 @@ export function info(message, data = {}) {
238
243
  */
239
244
  export function warn(message, data = {}) {
240
245
  stopSpinner();
246
+ writeLog('warn', message, data);
241
247
  if (!shouldLog('warn')) return;
242
248
  if (config.json) {
243
249
  console.error(JSON.stringify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.25.1",
3
+ "version": "0.26.1",
4
4
  "description": "Visual review platform for UI developers and designers",
5
5
  "keywords": [
6
6
  "visual-testing",
@@ -86,7 +86,7 @@
86
86
  "registry": "https://registry.npmjs.org/"
87
87
  },
88
88
  "dependencies": {
89
- "@vizzly-testing/honeydiff": "^0.9.0",
89
+ "@vizzly-testing/honeydiff": "^0.10.0",
90
90
  "ansis": "^4.2.0",
91
91
  "commander": "^14.0.0",
92
92
  "cosmiconfig": "^9.0.0",