@vizzly-testing/cli 0.1.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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +363 -0
  3. package/bin/vizzly.js +3 -0
  4. package/dist/cli.js +104 -0
  5. package/dist/client/index.js +237 -0
  6. package/dist/commands/doctor.js +158 -0
  7. package/dist/commands/init.js +102 -0
  8. package/dist/commands/run.js +224 -0
  9. package/dist/commands/status.js +164 -0
  10. package/dist/commands/tdd.js +212 -0
  11. package/dist/commands/upload.js +181 -0
  12. package/dist/container/index.js +184 -0
  13. package/dist/errors/vizzly-error.js +149 -0
  14. package/dist/index.js +31 -0
  15. package/dist/screenshot-wrapper.js +68 -0
  16. package/dist/sdk/index.js +364 -0
  17. package/dist/server/index.js +522 -0
  18. package/dist/services/api-service.js +215 -0
  19. package/dist/services/base-service.js +154 -0
  20. package/dist/services/build-manager.js +214 -0
  21. package/dist/services/screenshot-server.js +96 -0
  22. package/dist/services/server-manager.js +61 -0
  23. package/dist/services/service-utils.js +171 -0
  24. package/dist/services/tdd-service.js +444 -0
  25. package/dist/services/test-runner.js +210 -0
  26. package/dist/services/uploader.js +413 -0
  27. package/dist/types/cli.d.ts +2 -0
  28. package/dist/types/client/index.d.ts +76 -0
  29. package/dist/types/commands/doctor.d.ts +11 -0
  30. package/dist/types/commands/init.d.ts +14 -0
  31. package/dist/types/commands/run.d.ts +13 -0
  32. package/dist/types/commands/status.d.ts +13 -0
  33. package/dist/types/commands/tdd.d.ts +13 -0
  34. package/dist/types/commands/upload.d.ts +13 -0
  35. package/dist/types/container/index.d.ts +61 -0
  36. package/dist/types/errors/vizzly-error.d.ts +75 -0
  37. package/dist/types/index.d.ts +10 -0
  38. package/dist/types/index.js +153 -0
  39. package/dist/types/screenshot-wrapper.d.ts +27 -0
  40. package/dist/types/sdk/index.d.ts +108 -0
  41. package/dist/types/server/index.d.ts +38 -0
  42. package/dist/types/services/api-service.d.ts +77 -0
  43. package/dist/types/services/base-service.d.ts +72 -0
  44. package/dist/types/services/build-manager.d.ts +68 -0
  45. package/dist/types/services/screenshot-server.d.ts +10 -0
  46. package/dist/types/services/server-manager.d.ts +8 -0
  47. package/dist/types/services/service-utils.d.ts +45 -0
  48. package/dist/types/services/tdd-service.d.ts +55 -0
  49. package/dist/types/services/test-runner.d.ts +25 -0
  50. package/dist/types/services/uploader.d.ts +34 -0
  51. package/dist/types/types/index.d.ts +373 -0
  52. package/dist/types/utils/colors.d.ts +12 -0
  53. package/dist/types/utils/config-helpers.d.ts +6 -0
  54. package/dist/types/utils/config-loader.d.ts +22 -0
  55. package/dist/types/utils/console-ui.d.ts +61 -0
  56. package/dist/types/utils/diagnostics.d.ts +69 -0
  57. package/dist/types/utils/environment-config.d.ts +54 -0
  58. package/dist/types/utils/environment.d.ts +36 -0
  59. package/dist/types/utils/error-messages.d.ts +42 -0
  60. package/dist/types/utils/fetch-utils.d.ts +1 -0
  61. package/dist/types/utils/framework-detector.d.ts +5 -0
  62. package/dist/types/utils/git.d.ts +44 -0
  63. package/dist/types/utils/help.d.ts +11 -0
  64. package/dist/types/utils/image-comparison.d.ts +42 -0
  65. package/dist/types/utils/logger-factory.d.ts +26 -0
  66. package/dist/types/utils/logger.d.ts +79 -0
  67. package/dist/types/utils/package-info.d.ts +15 -0
  68. package/dist/types/utils/package.d.ts +1 -0
  69. package/dist/types/utils/project-detection.d.ts +19 -0
  70. package/dist/types/utils/ui-helpers.d.ts +23 -0
  71. package/dist/utils/colors.js +66 -0
  72. package/dist/utils/config-helpers.js +8 -0
  73. package/dist/utils/config-loader.js +120 -0
  74. package/dist/utils/console-ui.js +226 -0
  75. package/dist/utils/diagnostics.js +184 -0
  76. package/dist/utils/environment-config.js +93 -0
  77. package/dist/utils/environment.js +109 -0
  78. package/dist/utils/error-messages.js +34 -0
  79. package/dist/utils/fetch-utils.js +9 -0
  80. package/dist/utils/framework-detector.js +40 -0
  81. package/dist/utils/git.js +226 -0
  82. package/dist/utils/help.js +66 -0
  83. package/dist/utils/image-comparison.js +172 -0
  84. package/dist/utils/logger-factory.js +76 -0
  85. package/dist/utils/logger.js +231 -0
  86. package/dist/utils/package-info.js +38 -0
  87. package/dist/utils/package.js +9 -0
  88. package/dist/utils/project-detection.js +145 -0
  89. package/dist/utils/ui-helpers.js +86 -0
  90. package/package.json +103 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * API Service for Vizzly
3
+ * Handles HTTP requests to the Vizzly API
4
+ */
5
+
6
+ import { URLSearchParams } from 'url';
7
+ import { VizzlyError } from '../errors/vizzly-error.js';
8
+ import crypto from 'crypto';
9
+ import { getPackageVersion } from '../utils/package-info.js';
10
+ import { getApiUrl, getApiToken, getUserAgent } from '../utils/environment-config.js';
11
+
12
+ /**
13
+ * ApiService class for direct API communication
14
+ */
15
+ export class ApiService {
16
+ constructor(options = {}) {
17
+ this.baseUrl = options.baseUrl || getApiUrl();
18
+ this.token = options.token || getApiToken();
19
+
20
+ // Build User-Agent string
21
+ const command = options.command || 'run'; // Default to 'run' for API service
22
+ const baseUserAgent = `vizzly-cli/${getPackageVersion()} (${command})`;
23
+ const sdkUserAgent = options.userAgent || getUserAgent();
24
+ this.userAgent = sdkUserAgent ? `${baseUserAgent} ${sdkUserAgent}` : baseUserAgent;
25
+ if (!this.token && !options.allowNoToken) {
26
+ throw new VizzlyError('No API token provided. Set VIZZLY_TOKEN environment variable.');
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Make an API request
32
+ * @param {string} endpoint - API endpoint
33
+ * @param {Object} options - Fetch options
34
+ * @returns {Promise<Object>} Response data
35
+ */
36
+ async request(endpoint, options = {}) {
37
+ const url = `${this.baseUrl}${endpoint}`;
38
+ const headers = {
39
+ 'User-Agent': this.userAgent,
40
+ ...options.headers
41
+ };
42
+ if (this.token) {
43
+ headers.Authorization = `Bearer ${this.token}`;
44
+ }
45
+ const response = await fetch(url, {
46
+ ...options,
47
+ headers
48
+ });
49
+ if (!response.ok) {
50
+ let errorText = '';
51
+ try {
52
+ if (typeof response.text === 'function') {
53
+ errorText = await response.text();
54
+ } else {
55
+ errorText = response.statusText || '';
56
+ }
57
+ } catch {
58
+ // ignore
59
+ }
60
+ throw new VizzlyError(`API request failed: ${response.status}${errorText ? ` - ${errorText}` : ''} (URL: ${url})`);
61
+ }
62
+ return response.json();
63
+ }
64
+
65
+ /**
66
+ * Get build information
67
+ * @param {string} buildId - Build ID
68
+ * @param {string} include - Optional include parameter (e.g., 'screenshots')
69
+ * @returns {Promise<Object>} Build data
70
+ */
71
+ async getBuild(buildId, include = null) {
72
+ const endpoint = include ? `/api/sdk/builds/${buildId}?include=${include}` : `/api/sdk/builds/${buildId}`;
73
+ return this.request(endpoint);
74
+ }
75
+
76
+ /**
77
+ * Get comparison information
78
+ * @param {string} comparisonId - Comparison ID
79
+ * @returns {Promise<Object>} Comparison data
80
+ */
81
+ async getComparison(comparisonId) {
82
+ return this.request(`/api/sdk/comparisons/${comparisonId}`);
83
+ }
84
+
85
+ /**
86
+ * Get builds for a project
87
+ * @param {Object} filters - Filter options
88
+ * @returns {Promise<Array>} List of builds
89
+ */
90
+ async getBuilds(filters = {}) {
91
+ const queryParams = new URLSearchParams(filters).toString();
92
+ const endpoint = `/api/sdk/builds${queryParams ? `?${queryParams}` : ''}`;
93
+ return this.request(endpoint);
94
+ }
95
+
96
+ /**
97
+ * Create a new build
98
+ * @param {Object} metadata - Build metadata
99
+ * @returns {Promise<Object>} Created build data
100
+ */
101
+ async createBuild(metadata) {
102
+ return this.request('/api/sdk/builds', {
103
+ method: 'POST',
104
+ headers: {
105
+ 'Content-Type': 'application/json'
106
+ },
107
+ body: JSON.stringify(metadata)
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Check if SHAs already exist on the server
113
+ * @param {string[]} shas - Array of SHA256 hashes to check
114
+ * @returns {Promise<string[]>} Array of existing SHAs
115
+ */
116
+ async checkShas(shas) {
117
+ try {
118
+ const response = await this.request('/api/sdk/check-shas', {
119
+ method: 'POST',
120
+ headers: {
121
+ 'Content-Type': 'application/json'
122
+ },
123
+ body: JSON.stringify({
124
+ shas
125
+ })
126
+ });
127
+ return response.existing || [];
128
+ } catch (error) {
129
+ // Continue without deduplication on error
130
+ console.debug('SHA check failed, continuing without deduplication:', error.message);
131
+ return [];
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Upload a screenshot with SHA checking
137
+ * @param {string} buildId - Build ID
138
+ * @param {string} name - Screenshot name
139
+ * @param {Buffer} buffer - Screenshot data
140
+ * @param {Object} metadata - Additional metadata
141
+ * @returns {Promise<Object>} Upload result
142
+ */
143
+ async uploadScreenshot(buildId, name, buffer, metadata = {}) {
144
+ // Calculate SHA256 of the image
145
+ const sha256 = crypto.createHash('sha256').update(buffer).digest('hex');
146
+
147
+ // Check if this SHA already exists
148
+ const existingShas = await this.checkShas([sha256]);
149
+ if (existingShas.includes(sha256)) {
150
+ // File already exists, skip upload but still register the screenshot
151
+ return {
152
+ message: 'Screenshot already exists, skipped upload',
153
+ sha256,
154
+ skipped: true
155
+ };
156
+ }
157
+
158
+ // File doesn't exist, proceed with upload
159
+ return this.request(`/api/sdk/builds/${buildId}/screenshots`, {
160
+ method: 'POST',
161
+ headers: {
162
+ 'Content-Type': 'application/json'
163
+ },
164
+ body: JSON.stringify({
165
+ name,
166
+ image_data: buffer.toString('base64'),
167
+ properties: metadata ?? {},
168
+ sha256 // Include SHA for server-side deduplication
169
+ })
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Update build status
175
+ * @param {string} buildId - Build ID
176
+ * @param {string} status - Build status (pending|running|completed|failed)
177
+ * @param {number} executionTimeMs - Execution time in milliseconds
178
+ * @returns {Promise<Object>} Updated build data
179
+ */
180
+ async updateBuildStatus(buildId, status, executionTimeMs = null) {
181
+ const body = {
182
+ status
183
+ };
184
+ if (executionTimeMs !== null) {
185
+ body.executionTimeMs = executionTimeMs;
186
+ }
187
+ return this.request(`/api/sdk/builds/${buildId}/status`, {
188
+ method: 'PUT',
189
+ headers: {
190
+ 'Content-Type': 'application/json'
191
+ },
192
+ body: JSON.stringify(body)
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Finalize a build (convenience method)
198
+ * @param {string} buildId - Build ID
199
+ * @param {boolean} success - Whether the build succeeded
200
+ * @param {number} executionTimeMs - Execution time in milliseconds
201
+ * @returns {Promise<Object>} Finalized build data
202
+ */
203
+ async finalizeBuild(buildId, success = true, executionTimeMs = null) {
204
+ const status = success ? 'completed' : 'failed';
205
+ return this.updateBuildStatus(buildId, status, executionTimeMs);
206
+ }
207
+
208
+ /**
209
+ * Get token context (organization and project info)
210
+ * @returns {Promise<Object>} Token context data
211
+ */
212
+ async getTokenContext() {
213
+ return this.request('/api/sdk/token/context');
214
+ }
215
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Base Service Class
3
+ * Provides common functionality for all services
4
+ */
5
+
6
+ import { EventEmitter } from 'events';
7
+ import { VizzlyError } from '../errors/vizzly-error.js';
8
+ import { createStandardLogger } from '../utils/logger-factory.js';
9
+
10
+ /**
11
+ * @typedef {Object} ServiceOptions
12
+ * @property {Object} logger - Logger instance
13
+ * @property {AbortSignal} [signal] - Abort signal for cancellation
14
+ */
15
+
16
+ /**
17
+ * Base class for all services
18
+ * @extends EventEmitter
19
+ */
20
+ export class BaseService extends EventEmitter {
21
+ /**
22
+ * @param {Object} config - Service configuration
23
+ * @param {ServiceOptions} options - Service options
24
+ */
25
+ constructor(config, options = {}) {
26
+ super();
27
+ this.config = config;
28
+ this.logger = options.logger || createStandardLogger({
29
+ level: 'info'
30
+ });
31
+ this.signal = options.signal;
32
+ this.started = false;
33
+ this.stopping = false;
34
+
35
+ // Setup signal handling
36
+ if (this.signal) {
37
+ this.signal.addEventListener('abort', () => this.stop());
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Start the service
43
+ * @returns {Promise<void>}
44
+ */
45
+ async start() {
46
+ if (this.started) {
47
+ this.logger.warn(`${this.constructor.name} already started`);
48
+ return;
49
+ }
50
+ try {
51
+ this.emit('starting');
52
+ await this.onStart();
53
+ this.started = true;
54
+ this.emit('started');
55
+ } catch (error) {
56
+ this.emit('error', error);
57
+ throw new VizzlyError(`Failed to start ${this.constructor.name}`, 'SERVICE_START_FAILED', {
58
+ service: this.constructor.name,
59
+ error: error.message
60
+ });
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Stop the service
66
+ * @returns {Promise<void>}
67
+ */
68
+ async stop() {
69
+ if (!this.started || this.stopping) {
70
+ return;
71
+ }
72
+ this.stopping = true;
73
+ try {
74
+ this.emit('stopping');
75
+ await this.onStop();
76
+ this.started = false;
77
+ this.emit('stopped');
78
+ } catch (error) {
79
+ this.emit('error', error);
80
+ throw new VizzlyError(`Failed to stop ${this.constructor.name}`, 'SERVICE_STOP_FAILED', {
81
+ service: this.constructor.name,
82
+ error: error.message
83
+ });
84
+ } finally {
85
+ this.stopping = false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Hook for service-specific start logic
91
+ * @protected
92
+ * @returns {Promise<void>}
93
+ */
94
+ async onStart() {
95
+ // Override in subclasses
96
+ }
97
+
98
+ /**
99
+ * Hook for service-specific stop logic
100
+ * @protected
101
+ * @returns {Promise<void>}
102
+ */
103
+ async onStop() {
104
+ // Override in subclasses
105
+ }
106
+
107
+ /**
108
+ * Emit a progress event
109
+ * @param {string} phase - Progress phase
110
+ * @param {string} message - Progress message
111
+ * @param {Object} [data] - Additional data
112
+ */
113
+ emitProgress(phase, message, data = {}) {
114
+ this.emit('progress', {
115
+ phase,
116
+ message,
117
+ timestamp: new Date().toISOString(),
118
+ ...data
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Check if service is running
124
+ * @returns {boolean}
125
+ */
126
+ isRunning() {
127
+ return this.started && !this.stopping;
128
+ }
129
+
130
+ /**
131
+ * Wait for service to be ready
132
+ * @param {number} [timeout=30000] - Timeout in milliseconds
133
+ * @returns {Promise<void>}
134
+ */
135
+ async waitForReady(timeout = 30000) {
136
+ if (this.started) return;
137
+ return new Promise((resolve, reject) => {
138
+ const timer = setTimeout(() => {
139
+ reject(new VizzlyError('Service start timeout', 'SERVICE_TIMEOUT', {
140
+ service: this.constructor.name,
141
+ timeout
142
+ }));
143
+ }, timeout);
144
+ this.once('started', () => {
145
+ clearTimeout(timer);
146
+ resolve();
147
+ });
148
+ this.once('error', error => {
149
+ clearTimeout(timer);
150
+ reject(error);
151
+ });
152
+ });
153
+ }
154
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Build Manager Service
3
+ * Manages the build lifecycle and coordinates test execution
4
+ */
5
+
6
+ import crypto from 'crypto';
7
+ import { VizzlyError } from '../errors/vizzly-error.js';
8
+ import { BaseService } from './base-service.js';
9
+
10
+ /**
11
+ * Generate unique build ID for local build management only.
12
+ * Note: The API generates its own UUIDs for actual builds - this local ID
13
+ * is only used for CLI internal tracking and is not sent to the API.
14
+ * @returns {string} Build ID
15
+ */
16
+ export function generateBuildId() {
17
+ return `build-${crypto.randomUUID()}`;
18
+ }
19
+
20
+ /**
21
+ * Create build object
22
+ * @param {Object} buildOptions - Build configuration
23
+ * @returns {Object} Build object
24
+ */
25
+ export function createBuildObject(buildOptions) {
26
+ const {
27
+ name,
28
+ branch,
29
+ commit,
30
+ environment = 'test',
31
+ metadata = {}
32
+ } = buildOptions;
33
+ return {
34
+ id: generateBuildId(),
35
+ name: name || `build-${Date.now()}`,
36
+ branch,
37
+ commit,
38
+ environment,
39
+ metadata,
40
+ status: 'pending',
41
+ createdAt: new Date().toISOString(),
42
+ screenshots: []
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Update build with new status and data
48
+ * @param {Object} build - Current build
49
+ * @param {string} status - New status
50
+ * @param {Object} updates - Additional updates
51
+ * @returns {Object} Updated build
52
+ */
53
+ export function updateBuild(build, status, updates = {}) {
54
+ return {
55
+ ...build,
56
+ status,
57
+ updatedAt: new Date().toISOString(),
58
+ ...updates
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Add screenshot to build
64
+ * @param {Object} build - Current build
65
+ * @param {Object} screenshot - Screenshot data
66
+ * @returns {Object} Updated build
67
+ */
68
+ export function addScreenshotToBuild(build, screenshot) {
69
+ return {
70
+ ...build,
71
+ screenshots: [...build.screenshots, {
72
+ ...screenshot,
73
+ addedAt: new Date().toISOString()
74
+ }]
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Finalize build with result
80
+ * @param {Object} build - Current build
81
+ * @param {Object} result - Build result
82
+ * @returns {Object} Finalized build
83
+ */
84
+ export function finalizeBuildObject(build, result = {}) {
85
+ const finalStatus = result.success ? 'completed' : 'failed';
86
+ return {
87
+ ...build,
88
+ status: finalStatus,
89
+ completedAt: new Date().toISOString(),
90
+ result
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Create queued build item
96
+ * @param {Object} buildOptions - Build options
97
+ * @returns {Object} Queued build item
98
+ */
99
+ export function createQueuedBuild(buildOptions) {
100
+ return {
101
+ ...buildOptions,
102
+ queuedAt: new Date().toISOString()
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Validate build options
108
+ * @param {Object} buildOptions - Build options to validate
109
+ * @returns {Object} Validation result
110
+ */
111
+ export function validateBuildOptions(buildOptions) {
112
+ const errors = [];
113
+ if (!buildOptions.name && !buildOptions.branch) {
114
+ errors.push('Either name or branch is required');
115
+ }
116
+ if (buildOptions.environment && !['test', 'staging', 'production'].includes(buildOptions.environment)) {
117
+ errors.push('Environment must be one of: test, staging, production');
118
+ }
119
+ return {
120
+ valid: errors.length === 0,
121
+ errors
122
+ };
123
+ }
124
+ export class BuildManager extends BaseService {
125
+ constructor(config, logger) {
126
+ super(config, {
127
+ logger
128
+ });
129
+ this.currentBuild = null;
130
+ this.buildQueue = [];
131
+ }
132
+ async onStart() {
133
+ this.emitProgress('initializing', 0);
134
+ }
135
+ async onStop() {
136
+ if (this.currentBuild && this.currentBuild.status === 'pending') {
137
+ await this.updateBuildStatus(this.currentBuild.id, 'cancelled');
138
+ }
139
+ this.buildQueue.length = 0;
140
+ this.currentBuild = null;
141
+ }
142
+ async createBuild(buildOptions) {
143
+ this.emitProgress('creating', 'Creating new build...');
144
+ const build = createBuildObject(buildOptions);
145
+ this.currentBuild = build;
146
+ this.emitProgress('created', `Build created: ${build.name}`, {
147
+ build
148
+ });
149
+ return build;
150
+ }
151
+ async updateBuildStatus(buildId, status, updates = {}) {
152
+ if (!this.currentBuild || this.currentBuild.id !== buildId) {
153
+ throw new VizzlyError(`Build ${buildId} not found`, 'BUILD_NOT_FOUND');
154
+ }
155
+ this.currentBuild = updateBuild(this.currentBuild, status, updates);
156
+ this.emitProgress('updated', `Build status: ${status}`, {
157
+ buildId,
158
+ status,
159
+ build: this.currentBuild
160
+ });
161
+ return this.currentBuild;
162
+ }
163
+ async addScreenshot(buildId, screenshot) {
164
+ if (!this.currentBuild || this.currentBuild.id !== buildId) {
165
+ throw new VizzlyError(`Build ${buildId} not found`, 'BUILD_NOT_FOUND');
166
+ }
167
+ this.currentBuild = addScreenshotToBuild(this.currentBuild, screenshot);
168
+ this.emitProgress('screenshot-added', 'Screenshot added to build', {
169
+ buildId,
170
+ screenshotCount: this.currentBuild.screenshots.length,
171
+ screenshot
172
+ });
173
+ return this.currentBuild;
174
+ }
175
+ async finalizeBuild(buildId, result = {}) {
176
+ if (!this.currentBuild || this.currentBuild.id !== buildId) {
177
+ throw new VizzlyError(`Build ${buildId} not found`, 'BUILD_NOT_FOUND');
178
+ }
179
+ this.currentBuild = finalizeBuildObject(this.currentBuild, result);
180
+ this.emitProgress('finalized', `Build ${this.currentBuild.status}`, {
181
+ buildId,
182
+ build: this.currentBuild,
183
+ result
184
+ });
185
+ return this.currentBuild;
186
+ }
187
+ getCurrentBuild() {
188
+ return this.currentBuild;
189
+ }
190
+ queueBuild(buildOptions) {
191
+ const queuedBuild = createQueuedBuild(buildOptions);
192
+ this.buildQueue.push(queuedBuild);
193
+ this.emitProgress('queued', 'Build queued for processing', {
194
+ queueLength: this.buildQueue.length
195
+ });
196
+ }
197
+ async processNextBuild() {
198
+ if (this.buildQueue.length === 0) {
199
+ return null;
200
+ }
201
+ const buildOptions = this.buildQueue.shift();
202
+ return await this.createBuild(buildOptions);
203
+ }
204
+ getQueueStatus() {
205
+ return {
206
+ length: this.buildQueue.length,
207
+ items: this.buildQueue.map(item => ({
208
+ name: item.name,
209
+ branch: item.branch,
210
+ queuedAt: item.queuedAt
211
+ }))
212
+ };
213
+ }
214
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Screenshot Server Service
3
+ * Listens for and processes screenshots from the test runner
4
+ */
5
+
6
+ import { createServer } from 'http';
7
+ import { BaseService } from './base-service.js';
8
+ import { VizzlyError } from '../errors/vizzly-error.js';
9
+ export class ScreenshotServer extends BaseService {
10
+ constructor(config, logger, buildManager) {
11
+ super(config, logger);
12
+ this.buildManager = buildManager;
13
+ this.server = null;
14
+ }
15
+ async onStart() {
16
+ this.server = createServer(this.handleRequest.bind(this));
17
+ return new Promise((resolve, reject) => {
18
+ this.server.listen(this.config.server.port, '127.0.0.1', error => {
19
+ if (error) {
20
+ reject(new VizzlyError(`Failed to start screenshot server: ${error.message}`, 'SERVER_ERROR'));
21
+ } else {
22
+ this.logger.info(`Screenshot server listening on http://127.0.0.1:${this.config.server.port}`);
23
+ resolve();
24
+ }
25
+ });
26
+ });
27
+ }
28
+ async onStop() {
29
+ if (this.server) {
30
+ return new Promise(resolve => {
31
+ this.server.close(() => {
32
+ this.logger.info('Screenshot server stopped');
33
+ resolve();
34
+ });
35
+ });
36
+ }
37
+ }
38
+ async handleRequest(req, res) {
39
+ if (req.method === 'POST' && req.url === '/screenshot') {
40
+ try {
41
+ const body = await this.parseRequestBody(req);
42
+ const {
43
+ buildId,
44
+ name,
45
+ image,
46
+ properties
47
+ } = body;
48
+ if (!buildId || !name || !image) {
49
+ res.statusCode = 400;
50
+ res.end(JSON.stringify({
51
+ error: 'buildId, name, and image are required'
52
+ }));
53
+ return;
54
+ }
55
+ await this.buildManager.addScreenshot(buildId, {
56
+ name,
57
+ image,
58
+ properties
59
+ });
60
+ res.statusCode = 200;
61
+ res.end(JSON.stringify({
62
+ success: true
63
+ }));
64
+ } catch (error) {
65
+ this.logger.error('Failed to process screenshot:', error);
66
+ res.statusCode = 500;
67
+ res.end(JSON.stringify({
68
+ error: 'Internal server error'
69
+ }));
70
+ }
71
+ } else {
72
+ res.statusCode = 404;
73
+ res.end(JSON.stringify({
74
+ error: 'Not found'
75
+ }));
76
+ }
77
+ }
78
+ async parseRequestBody(req) {
79
+ return new Promise((resolve, reject) => {
80
+ let body = '';
81
+ req.on('data', chunk => {
82
+ body += chunk.toString();
83
+ });
84
+ req.on('end', () => {
85
+ try {
86
+ resolve(JSON.parse(body));
87
+ } catch {
88
+ reject(new VizzlyError('Invalid JSON in request body', 'INVALID_JSON'));
89
+ }
90
+ });
91
+ req.on('error', error => {
92
+ reject(new VizzlyError(`Request error: ${error.message}`, 'REQUEST_ERROR'));
93
+ });
94
+ });
95
+ }
96
+ }