@vizzly-testing/cli 0.10.1 → 0.10.2

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.
@@ -1,4 +1,4 @@
1
- import { writeFileSync, readFileSync, existsSync, unlinkSync, mkdirSync, openSync } from 'fs';
1
+ import { writeFileSync, readFileSync, existsSync, unlinkSync, mkdirSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { spawn } from 'child_process';
4
4
  import { ConsoleUI } from '../utils/console-ui.js';
@@ -36,21 +36,56 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
36
36
  }
37
37
  const port = options.port || 47392;
38
38
 
39
- // Prepare log files for daemon output
40
- const logFile = join(vizzlyDir, 'daemon.log');
41
- const errorFile = join(vizzlyDir, 'daemon-error.log');
39
+ // Show loading indicator if downloading baselines (but not in verbose mode since child shows progress)
40
+ if (options.baselineBuild && !globalOptions.verbose) {
41
+ ui.startSpinner(`Downloading baselines from build ${options.baselineBuild}...`);
42
+ }
42
43
 
43
- // Spawn detached child process to run the server
44
+ // Spawn child process with stdio inherited during init for direct error visibility
44
45
  const child = spawn(process.execPath, [process.argv[1],
45
46
  // CLI entry point
46
47
  'tdd', 'start', '--daemon-child',
47
48
  // Special flag for child process
48
49
  '--port', port.toString(), ...(options.open ? ['--open'] : []), ...(options.baselineBuild ? ['--baseline-build', options.baselineBuild] : []), ...(options.baselineComparison ? ['--baseline-comparison', options.baselineComparison] : []), ...(options.environment ? ['--environment', options.environment] : []), ...(options.threshold !== undefined ? ['--threshold', options.threshold.toString()] : []), ...(options.timeout ? ['--timeout', options.timeout] : []), ...(options.token ? ['--token', options.token] : []), ...(globalOptions.json ? ['--json'] : []), ...(globalOptions.verbose ? ['--verbose'] : []), ...(globalOptions.noColor ? ['--no-color'] : [])], {
49
50
  detached: true,
50
- stdio: ['ignore', openSync(logFile, 'a'), openSync(errorFile, 'a')],
51
+ stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
51
52
  cwd: process.cwd()
52
53
  });
53
54
 
55
+ // Wait for child to signal successful init or exit with error
56
+ let initComplete = false;
57
+ let initFailed = false;
58
+ await new Promise(resolve => {
59
+ // Child disconnects IPC when initialization succeeds
60
+ child.on('disconnect', () => {
61
+ initComplete = true;
62
+ resolve();
63
+ });
64
+
65
+ // Child exits before disconnecting = initialization failed
66
+ child.on('exit', () => {
67
+ if (!initComplete) {
68
+ initFailed = true;
69
+ resolve();
70
+ }
71
+ });
72
+
73
+ // Timeout after 30 seconds to prevent indefinite wait
74
+ setTimeout(() => {
75
+ if (!initComplete && !initFailed) {
76
+ initFailed = true;
77
+ resolve();
78
+ }
79
+ }, 30000);
80
+ });
81
+ if (initFailed) {
82
+ if (options.baselineBuild && !globalOptions.verbose) {
83
+ ui.stopSpinner();
84
+ }
85
+ ui.error('TDD server failed to start');
86
+ process.exit(1);
87
+ }
88
+
54
89
  // Unref so parent can exit
55
90
  child.unref();
56
91
 
@@ -62,6 +97,9 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
62
97
  await new Promise(resolve => setTimeout(resolve, retryDelay * (i + 1)));
63
98
  running = await isServerRunning(port);
64
99
  }
100
+ if (options.baselineBuild && !globalOptions.verbose) {
101
+ ui.stopSpinner();
102
+ }
65
103
  if (!running) {
66
104
  ui.error('Failed to start TDD server - server not responding to health checks');
67
105
  process.exit(1);
@@ -106,6 +144,11 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
106
144
  daemon: true
107
145
  }, globalOptions);
108
146
 
147
+ // Disconnect IPC after successful initialization to signal parent
148
+ if (process.send) {
149
+ process.disconnect();
150
+ }
151
+
109
152
  // Store our PID for the stop command
110
153
  const pidFile = join(vizzlyDir, 'server.pid');
111
154
  writeFileSync(pidFile, process.pid.toString());
@@ -139,15 +182,8 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
139
182
  // Keep process alive
140
183
  process.stdin.resume();
141
184
  } catch (error) {
142
- // Log error to file for debugging
143
- const logFile = join(vizzlyDir, 'daemon-error.log');
144
- try {
145
- writeFileSync(logFile, `[${new Date().toISOString()}] ${error.stack || error}\n`, {
146
- flag: 'a'
147
- });
148
- } catch {
149
- // Silent failure if we can't write log
150
- }
185
+ // Most errors shown via inherited stdio, but catch any that weren't
186
+ console.error(`Fatal error: ${error.message}`);
151
187
  process.exit(1);
152
188
  }
153
189
  }
@@ -53,7 +53,9 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
53
53
  // Collect git metadata
54
54
  const branch = await detectBranch(options.branch);
55
55
  const commit = await detectCommit(options.commit);
56
- if (globalOptions.verbose) {
56
+
57
+ // Only show config in verbose mode for non-daemon (daemon shows baseline info instead)
58
+ if (globalOptions.verbose && !options.daemon) {
57
59
  ui.info('TDD Configuration loaded', {
58
60
  testCommand,
59
61
  port: config.server.port,
@@ -97,8 +99,13 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
97
99
  ui.info(`TDD screenshot server running on port ${serverInfo.port}`);
98
100
  ui.info(`Dashboard: http://localhost:${serverInfo.port}/dashboard`);
99
101
  }
100
- if (globalOptions.verbose) {
101
- ui.info('Server details', serverInfo);
102
+ // Verbose server details only in non-daemon mode
103
+ if (globalOptions.verbose && !options.daemon) {
104
+ ui.info('Server started', {
105
+ port: serverInfo.port,
106
+ pid: serverInfo.pid,
107
+ uptime: serverInfo.uptime
108
+ });
102
109
  }
103
110
  });
104
111
  testRunner.on('screenshot-captured', screenshotInfo => {
@@ -90,8 +90,6 @@ export class TddService {
90
90
  });
91
91
  }
92
92
  async downloadBaselines(environment = 'test', branch = null, buildId = null, comparisonId = null) {
93
- logger.info('🔍 Looking for baseline build...');
94
-
95
93
  // If no branch specified, try to detect the default branch
96
94
  if (!branch) {
97
95
  branch = await getDefaultBranch();
@@ -107,7 +105,6 @@ export class TddService {
107
105
  let baselineBuild;
108
106
  if (buildId) {
109
107
  // Use specific build ID - get it with screenshots in one call
110
- logger.info(`📌 Using specified build: ${buildId}`);
111
108
  const apiResponse = await this.api.getBuild(buildId, 'screenshots');
112
109
 
113
110
  // Debug the full API response (only in debug mode)
@@ -154,7 +151,6 @@ export class TddService {
154
151
  }
155
152
  baselineBuild = builds.data[0];
156
153
  }
157
- logger.info(`📥 Found baseline build: ${colors.cyan(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
158
154
 
159
155
  // For specific buildId, we already have screenshots, otherwise get build details
160
156
  let buildDetails = baselineBuild;
@@ -167,7 +163,8 @@ export class TddService {
167
163
  logger.warn('⚠️ No screenshots found in baseline build');
168
164
  return null;
169
165
  }
170
- logger.info(`📸 Downloading ${colors.cyan(buildDetails.screenshots.length)} baseline screenshots...`);
166
+ logger.info(`Using baseline from build: ${colors.cyan(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
167
+ logger.info(`Checking ${colors.cyan(buildDetails.screenshots.length)} baseline screenshots...`);
171
168
 
172
169
  // Debug screenshots structure (only in debug mode)
173
170
  logger.debug(`📊 Screenshots array structure:`, {
@@ -352,17 +349,37 @@ export class TddService {
352
349
  const metadataPath = join(this.baselinePath, 'metadata.json');
353
350
  writeFileSync(metadataPath, JSON.stringify(this.baselineData, null, 2));
354
351
 
352
+ // Save baseline build metadata for MCP plugin
353
+ const baselineMetadataPath = safePath(this.workingDir, '.vizzly', 'baseline-metadata.json');
354
+ const buildMetadata = {
355
+ buildId: baselineBuild.id,
356
+ buildName: baselineBuild.name,
357
+ branch: branch,
358
+ environment: environment,
359
+ commitSha: baselineBuild.commit_sha,
360
+ commitMessage: baselineBuild.commit_message,
361
+ approvalStatus: baselineBuild.approval_status,
362
+ completedAt: baselineBuild.completed_at,
363
+ downloadedAt: new Date().toISOString()
364
+ };
365
+ writeFileSync(baselineMetadataPath, JSON.stringify(buildMetadata, null, 2));
366
+
355
367
  // Final summary
356
368
  const actualDownloads = downloadedCount - skippedCount;
357
- const totalAttempted = downloadedCount + errorCount;
358
- if (skippedCount > 0 || errorCount > 0) {
359
- let summaryParts = [];
360
- if (actualDownloads > 0) summaryParts.push(`${actualDownloads} downloaded`);
361
- if (skippedCount > 0) summaryParts.push(`${skippedCount} skipped (matching SHA)`);
362
- if (errorCount > 0) summaryParts.push(`${errorCount} failed`);
363
- logger.info(`✅ Baseline ready - ${summaryParts.join(', ')} - ${totalAttempted}/${buildDetails.screenshots.length} total`);
369
+ if (skippedCount > 0) {
370
+ // All skipped (up-to-date)
371
+ if (actualDownloads === 0) {
372
+ logger.info(`✅ All ${skippedCount} baselines up-to-date (matching local SHA)`);
373
+ } else {
374
+ // Mixed: some downloaded, some skipped
375
+ logger.info(`✅ Downloaded ${actualDownloads} new screenshots, ${skippedCount} already up-to-date`);
376
+ }
364
377
  } else {
365
- logger.info(`✅ Baseline downloaded successfully - ${downloadedCount}/${buildDetails.screenshots.length} screenshots`);
378
+ // Fresh download
379
+ logger.info(`✅ Downloaded ${downloadedCount}/${buildDetails.screenshots.length} screenshots successfully`);
380
+ }
381
+ if (errorCount > 0) {
382
+ logger.warn(`⚠️ ${errorCount} screenshots failed to download`);
366
383
  }
367
384
  return this.baselineData;
368
385
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Visual review platform for UI developers and designers",
5
5
  "keywords": [
6
6
  "visual-testing",