@vizzly-testing/cli 0.13.1 → 0.13.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.
Files changed (40) hide show
  1. package/README.md +552 -88
  2. package/claude-plugin/.claude-plugin/README.md +4 -0
  3. package/claude-plugin/.mcp.json +4 -0
  4. package/claude-plugin/CHANGELOG.md +27 -0
  5. package/claude-plugin/mcp/vizzly-docs-server/README.md +95 -0
  6. package/claude-plugin/mcp/vizzly-docs-server/docs-fetcher.js +110 -0
  7. package/claude-plugin/mcp/vizzly-docs-server/index.js +283 -0
  8. package/claude-plugin/mcp/vizzly-server/cloud-api-provider.js +26 -10
  9. package/claude-plugin/mcp/vizzly-server/index.js +14 -1
  10. package/claude-plugin/mcp/vizzly-server/local-tdd-provider.js +61 -28
  11. package/dist/cli.js +4 -4
  12. package/dist/commands/run.js +1 -1
  13. package/dist/commands/tdd-daemon.js +54 -8
  14. package/dist/commands/tdd.js +8 -8
  15. package/dist/container/index.js +34 -3
  16. package/dist/reporter/reporter-bundle.css +1 -1
  17. package/dist/reporter/reporter-bundle.iife.js +29 -59
  18. package/dist/server/handlers/tdd-handler.js +18 -16
  19. package/dist/server/http-server.js +473 -4
  20. package/dist/services/config-service.js +371 -0
  21. package/dist/services/project-service.js +245 -0
  22. package/dist/services/server-manager.js +4 -5
  23. package/dist/services/static-report-generator.js +208 -0
  24. package/dist/services/tdd-service.js +14 -6
  25. package/dist/types/reporter/src/components/ui/form-field.d.ts +16 -0
  26. package/dist/types/reporter/src/components/views/projects-view.d.ts +1 -0
  27. package/dist/types/reporter/src/components/views/settings-view.d.ts +1 -0
  28. package/dist/types/reporter/src/hooks/use-auth.d.ts +10 -0
  29. package/dist/types/reporter/src/hooks/use-config.d.ts +9 -0
  30. package/dist/types/reporter/src/hooks/use-projects.d.ts +10 -0
  31. package/dist/types/reporter/src/services/api-client.d.ts +7 -0
  32. package/dist/types/server/http-server.d.ts +1 -1
  33. package/dist/types/services/config-service.d.ts +98 -0
  34. package/dist/types/services/project-service.d.ts +103 -0
  35. package/dist/types/services/server-manager.d.ts +2 -1
  36. package/dist/types/services/static-report-generator.d.ts +25 -0
  37. package/dist/types/services/tdd-service.d.ts +2 -2
  38. package/dist/utils/console-ui.js +26 -2
  39. package/docs/tdd-mode.md +31 -15
  40. package/package.json +4 -4
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Configuration Service
3
+ * Manages reading and writing Vizzly configuration files
4
+ */
5
+
6
+ import { BaseService } from './base-service.js';
7
+ import { cosmiconfigSync } from 'cosmiconfig';
8
+ import { writeFile, readFile } from 'fs/promises';
9
+ import { join } from 'path';
10
+ import { VizzlyError } from '../errors/vizzly-error.js';
11
+ import { validateVizzlyConfigWithDefaults } from '../utils/config-schema.js';
12
+ import { loadGlobalConfig, saveGlobalConfig, getGlobalConfigPath } from '../utils/global-config.js';
13
+
14
+ /**
15
+ * ConfigService for reading and writing configuration
16
+ * @extends BaseService
17
+ */
18
+ export class ConfigService extends BaseService {
19
+ constructor(config, options = {}) {
20
+ super(config, options);
21
+ this.projectRoot = options.projectRoot || process.cwd();
22
+ this.explorer = cosmiconfigSync('vizzly');
23
+ }
24
+
25
+ /**
26
+ * Get configuration with source information
27
+ * @param {string} scope - 'project', 'global', or 'merged'
28
+ * @returns {Promise<Object>} Config object with metadata
29
+ */
30
+ async getConfig(scope = 'merged') {
31
+ if (scope === 'project') {
32
+ return this._getProjectConfig();
33
+ }
34
+ if (scope === 'global') {
35
+ return this._getGlobalConfig();
36
+ }
37
+ if (scope === 'merged') {
38
+ return this._getMergedConfig();
39
+ }
40
+ throw new VizzlyError(`Invalid config scope: ${scope}. Must be 'project', 'global', or 'merged'`, 'INVALID_CONFIG_SCOPE');
41
+ }
42
+
43
+ /**
44
+ * Get project-level config from vizzly.config.js or similar
45
+ * @private
46
+ * @returns {Promise<Object>}
47
+ */
48
+ async _getProjectConfig() {
49
+ let result = this.explorer.search(this.projectRoot);
50
+ if (!result || !result.config) {
51
+ return {
52
+ config: {},
53
+ filepath: null,
54
+ isEmpty: true
55
+ };
56
+ }
57
+ let config = result.config.default || result.config;
58
+ return {
59
+ config,
60
+ filepath: result.filepath,
61
+ isEmpty: Object.keys(config).length === 0
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Get global config from ~/.vizzly/config.json
67
+ * @private
68
+ * @returns {Promise<Object>}
69
+ */
70
+ async _getGlobalConfig() {
71
+ let globalConfig = await loadGlobalConfig();
72
+ return {
73
+ config: globalConfig,
74
+ filepath: getGlobalConfigPath(),
75
+ isEmpty: Object.keys(globalConfig).length === 0
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Get merged config showing source for each setting
81
+ * @private
82
+ * @returns {Promise<Object>}
83
+ */
84
+ async _getMergedConfig() {
85
+ let projectConfigData = await this._getProjectConfig();
86
+ let globalConfigData = await this._getGlobalConfig();
87
+
88
+ // Build config with source tracking
89
+ let mergedConfig = {};
90
+ let sources = {};
91
+
92
+ // Layer 1: Defaults
93
+ let defaults = {
94
+ apiUrl: 'https://app.vizzly.dev',
95
+ server: {
96
+ port: 47392,
97
+ timeout: 30000
98
+ },
99
+ build: {
100
+ name: 'Build {timestamp}',
101
+ environment: 'test'
102
+ },
103
+ upload: {
104
+ screenshotsDir: './screenshots',
105
+ batchSize: 10,
106
+ timeout: 30000
107
+ },
108
+ comparison: {
109
+ threshold: 0.1
110
+ },
111
+ tdd: {
112
+ openReport: false
113
+ },
114
+ plugins: []
115
+ };
116
+ Object.keys(defaults).forEach(key => {
117
+ mergedConfig[key] = defaults[key];
118
+ sources[key] = 'default';
119
+ });
120
+
121
+ // Layer 2: Global config (auth, project mappings, user preferences)
122
+ if (globalConfigData.config.auth) {
123
+ mergedConfig.auth = globalConfigData.config.auth;
124
+ sources.auth = 'global';
125
+ }
126
+ if (globalConfigData.config.projects) {
127
+ mergedConfig.projects = globalConfigData.config.projects;
128
+ sources.projects = 'global';
129
+ }
130
+
131
+ // Layer 3: Project config file
132
+ Object.keys(projectConfigData.config).forEach(key => {
133
+ mergedConfig[key] = projectConfigData.config[key];
134
+ sources[key] = 'project';
135
+ });
136
+
137
+ // Layer 4: Environment variables (tracked separately)
138
+ let envOverrides = {};
139
+ if (process.env.VIZZLY_TOKEN) {
140
+ envOverrides.apiKey = process.env.VIZZLY_TOKEN;
141
+ sources.apiKey = 'env';
142
+ }
143
+ if (process.env.VIZZLY_API_URL) {
144
+ envOverrides.apiUrl = process.env.VIZZLY_API_URL;
145
+ sources.apiUrl = 'env';
146
+ }
147
+ return {
148
+ config: {
149
+ ...mergedConfig,
150
+ ...envOverrides
151
+ },
152
+ sources,
153
+ projectFilepath: projectConfigData.filepath,
154
+ globalFilepath: globalConfigData.filepath
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Update configuration
160
+ * @param {string} scope - 'project' or 'global'
161
+ * @param {Object} updates - Configuration updates to apply
162
+ * @returns {Promise<Object>} Updated config
163
+ */
164
+ async updateConfig(scope, updates) {
165
+ if (scope === 'project') {
166
+ return this._updateProjectConfig(updates);
167
+ }
168
+ if (scope === 'global') {
169
+ return this._updateGlobalConfig(updates);
170
+ }
171
+ throw new VizzlyError(`Invalid config scope for update: ${scope}. Must be 'project' or 'global'`, 'INVALID_CONFIG_SCOPE');
172
+ }
173
+
174
+ /**
175
+ * Update project-level config
176
+ * @private
177
+ * @param {Object} updates - Config updates
178
+ * @returns {Promise<Object>} Updated config
179
+ */
180
+ async _updateProjectConfig(updates) {
181
+ let result = this.explorer.search(this.projectRoot);
182
+
183
+ // Determine config file path
184
+ let configPath;
185
+ let currentConfig = {};
186
+ if (result && result.filepath) {
187
+ configPath = result.filepath;
188
+ currentConfig = result.config.default || result.config;
189
+ } else {
190
+ // Create new config file - prefer vizzly.config.js
191
+ configPath = join(this.projectRoot, 'vizzly.config.js');
192
+ }
193
+
194
+ // Merge updates with current config
195
+ let newConfig = this._deepMerge(currentConfig, updates);
196
+
197
+ // Validate before writing
198
+ try {
199
+ validateVizzlyConfigWithDefaults(newConfig);
200
+ } catch (error) {
201
+ throw new VizzlyError(`Invalid configuration: ${error.message}`, 'CONFIG_VALIDATION_ERROR', {
202
+ errors: error.errors
203
+ });
204
+ }
205
+
206
+ // Write config file
207
+ await this._writeProjectConfigFile(configPath, newConfig);
208
+
209
+ // Clear cosmiconfig cache
210
+ this.explorer.clearCaches();
211
+ return {
212
+ config: newConfig,
213
+ filepath: configPath
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Update global config
219
+ * @private
220
+ * @param {Object} updates - Config updates
221
+ * @returns {Promise<Object>} Updated config
222
+ */
223
+ async _updateGlobalConfig(updates) {
224
+ let currentConfig = await loadGlobalConfig();
225
+ let newConfig = this._deepMerge(currentConfig, updates);
226
+ await saveGlobalConfig(newConfig);
227
+ return {
228
+ config: newConfig,
229
+ filepath: getGlobalConfigPath()
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Write project config file (JavaScript format)
235
+ * @private
236
+ * @param {string} filepath - Path to write to
237
+ * @param {Object} config - Config object
238
+ * @returns {Promise<void>}
239
+ */
240
+ async _writeProjectConfigFile(filepath, config) {
241
+ // For .js files, export as ES module
242
+ if (filepath.endsWith('.js') || filepath.endsWith('.mjs')) {
243
+ let content = this._serializeToJavaScript(config);
244
+ await writeFile(filepath, content, 'utf-8');
245
+ return;
246
+ }
247
+
248
+ // For .json files
249
+ if (filepath.endsWith('.json')) {
250
+ let content = JSON.stringify(config, null, 2);
251
+ await writeFile(filepath, content, 'utf-8');
252
+ return;
253
+ }
254
+
255
+ // For package.json, merge into existing
256
+ if (filepath.endsWith('package.json')) {
257
+ let pkgContent = await readFile(filepath, 'utf-8');
258
+ let pkg = JSON.parse(pkgContent);
259
+ pkg.vizzly = config;
260
+ await writeFile(filepath, JSON.stringify(pkg, null, 2), 'utf-8');
261
+ return;
262
+ }
263
+ throw new VizzlyError(`Unsupported config file format: ${filepath}`, 'UNSUPPORTED_CONFIG_FORMAT');
264
+ }
265
+
266
+ /**
267
+ * Serialize config object to JavaScript module
268
+ * @private
269
+ * @param {Object} config - Config object
270
+ * @returns {string} JavaScript source code
271
+ */
272
+ _serializeToJavaScript(config) {
273
+ let lines = ['/**', ' * Vizzly Configuration', ' * @see https://docs.vizzly.dev/cli/configuration', ' */', '', "import { defineConfig } from '@vizzly-testing/cli/config';", '', 'export default defineConfig(', this._stringifyWithIndent(config, 1), ');', ''];
274
+ return lines.join('\n');
275
+ }
276
+
277
+ /**
278
+ * Stringify object with proper indentation (2 spaces)
279
+ * @private
280
+ * @param {*} value - Value to stringify
281
+ * @param {number} depth - Current depth
282
+ * @returns {string}
283
+ */
284
+ _stringifyWithIndent(value, depth = 0) {
285
+ let indent = ' '.repeat(depth);
286
+ let prevIndent = ' '.repeat(depth - 1);
287
+ if (value === null || value === undefined) {
288
+ return String(value);
289
+ }
290
+ if (typeof value === 'string') {
291
+ return `'${value.replace(/'/g, "\\'")}'`;
292
+ }
293
+ if (typeof value === 'number' || typeof value === 'boolean') {
294
+ return String(value);
295
+ }
296
+ if (Array.isArray(value)) {
297
+ if (value.length === 0) return '[]';
298
+ let items = value.map(item => `${indent}${this._stringifyWithIndent(item, depth + 1)}`);
299
+ return `[\n${items.join(',\n')}\n${prevIndent}]`;
300
+ }
301
+ if (typeof value === 'object') {
302
+ let keys = Object.keys(value);
303
+ if (keys.length === 0) return '{}';
304
+ let items = keys.map(key => {
305
+ let val = this._stringifyWithIndent(value[key], depth + 1);
306
+ return `${indent}${key}: ${val}`;
307
+ });
308
+ return `{\n${items.join(',\n')}\n${prevIndent}}`;
309
+ }
310
+ return String(value);
311
+ }
312
+
313
+ /**
314
+ * Validate configuration object
315
+ * @param {Object} config - Config to validate
316
+ * @returns {Promise<Object>} Validation result
317
+ */
318
+ async validateConfig(config) {
319
+ try {
320
+ let validated = validateVizzlyConfigWithDefaults(config);
321
+ return {
322
+ valid: true,
323
+ config: validated,
324
+ errors: []
325
+ };
326
+ } catch (error) {
327
+ return {
328
+ valid: false,
329
+ config: null,
330
+ errors: error.errors || [{
331
+ message: error.message
332
+ }]
333
+ };
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Get the source of a specific config key
339
+ * @param {string} key - Config key
340
+ * @returns {Promise<string>} Source ('default', 'global', 'project', 'env', 'cli')
341
+ */
342
+ async getConfigSource(key) {
343
+ let merged = await this._getMergedConfig();
344
+ return merged.sources[key] || 'unknown';
345
+ }
346
+
347
+ /**
348
+ * Deep merge two objects
349
+ * @private
350
+ * @param {Object} target - Target object
351
+ * @param {Object} source - Source object
352
+ * @returns {Object} Merged object
353
+ */
354
+ _deepMerge(target, source) {
355
+ let output = {
356
+ ...target
357
+ };
358
+ for (let key in source) {
359
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
360
+ if (target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])) {
361
+ output[key] = this._deepMerge(target[key], source[key]);
362
+ } else {
363
+ output[key] = source[key];
364
+ }
365
+ } else {
366
+ output[key] = source[key];
367
+ }
368
+ }
369
+ return output;
370
+ }
371
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Project Service
3
+ * Manages project mappings and project-related operations
4
+ */
5
+
6
+ import { BaseService } from './base-service.js';
7
+ import { VizzlyError } from '../errors/vizzly-error.js';
8
+ import { getProjectMappings, saveProjectMapping, deleteProjectMapping, getProjectMapping } from '../utils/global-config.js';
9
+
10
+ /**
11
+ * ProjectService for managing project mappings and operations
12
+ * @extends BaseService
13
+ */
14
+ export class ProjectService extends BaseService {
15
+ constructor(config, options = {}) {
16
+ super(config, options);
17
+ this.apiService = options.apiService;
18
+ }
19
+
20
+ /**
21
+ * List all project mappings
22
+ * @returns {Promise<Array>} Array of project mappings
23
+ */
24
+ async listMappings() {
25
+ let mappings = await getProjectMappings();
26
+
27
+ // Convert object to array with directory path included
28
+ return Object.entries(mappings).map(([directory, data]) => ({
29
+ directory,
30
+ ...data
31
+ }));
32
+ }
33
+
34
+ /**
35
+ * Get project mapping for a specific directory
36
+ * @param {string} directory - Directory path
37
+ * @returns {Promise<Object|null>} Project mapping or null
38
+ */
39
+ async getMapping(directory) {
40
+ return getProjectMapping(directory);
41
+ }
42
+
43
+ /**
44
+ * Create or update project mapping
45
+ * @param {string} directory - Directory path
46
+ * @param {Object} projectData - Project data
47
+ * @param {string} projectData.projectSlug - Project slug
48
+ * @param {string} projectData.organizationSlug - Organization slug
49
+ * @param {string} projectData.token - Project API token
50
+ * @param {string} [projectData.projectName] - Optional project name
51
+ * @returns {Promise<Object>} Created mapping
52
+ */
53
+ async createMapping(directory, projectData) {
54
+ if (!directory) {
55
+ throw new VizzlyError('Directory path is required', 'INVALID_DIRECTORY');
56
+ }
57
+ if (!projectData.projectSlug) {
58
+ throw new VizzlyError('Project slug is required', 'INVALID_PROJECT_DATA');
59
+ }
60
+ if (!projectData.organizationSlug) {
61
+ throw new VizzlyError('Organization slug is required', 'INVALID_PROJECT_DATA');
62
+ }
63
+ if (!projectData.token) {
64
+ throw new VizzlyError('Project token is required', 'INVALID_PROJECT_DATA');
65
+ }
66
+ await saveProjectMapping(directory, projectData);
67
+ return {
68
+ directory,
69
+ ...projectData
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Remove project mapping
75
+ * @param {string} directory - Directory path
76
+ * @returns {Promise<void>}
77
+ */
78
+ async removeMapping(directory) {
79
+ if (!directory) {
80
+ throw new VizzlyError('Directory path is required', 'INVALID_DIRECTORY');
81
+ }
82
+ await deleteProjectMapping(directory);
83
+ }
84
+
85
+ /**
86
+ * Switch project for current directory
87
+ * @param {string} projectSlug - Project slug
88
+ * @param {string} organizationSlug - Organization slug
89
+ * @param {string} token - Project token
90
+ * @returns {Promise<Object>} Updated mapping
91
+ */
92
+ async switchProject(projectSlug, organizationSlug, token) {
93
+ let currentDir = process.cwd();
94
+ return this.createMapping(currentDir, {
95
+ projectSlug,
96
+ organizationSlug,
97
+ token
98
+ });
99
+ }
100
+
101
+ /**
102
+ * List all projects from API
103
+ * @returns {Promise<Array>} Array of projects
104
+ */
105
+ async listProjects() {
106
+ if (!this.apiService) {
107
+ // Return empty array if not authenticated - this is expected in local mode
108
+ return [];
109
+ }
110
+ try {
111
+ let response = await this.apiService.request('/api/cli/projects', {
112
+ method: 'GET'
113
+ });
114
+ return response.projects || [];
115
+ } catch {
116
+ // Return empty array on error - likely not authenticated
117
+ return [];
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Get project details
123
+ * @param {string} projectSlug - Project slug
124
+ * @param {string} organizationSlug - Organization slug
125
+ * @returns {Promise<Object>} Project details
126
+ */
127
+ async getProject(projectSlug, organizationSlug) {
128
+ if (!this.apiService) {
129
+ throw new VizzlyError('API service not available', 'NO_API_SERVICE');
130
+ }
131
+ try {
132
+ let response = await this.apiService.request(`/api/cli/organizations/${organizationSlug}/projects/${projectSlug}`, {
133
+ method: 'GET'
134
+ });
135
+ return response.project;
136
+ } catch (error) {
137
+ throw new VizzlyError(`Failed to fetch project: ${error.message}`, 'PROJECT_FETCH_FAILED', {
138
+ originalError: error
139
+ });
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Get recent builds for a project
145
+ * @param {string} projectSlug - Project slug
146
+ * @param {string} organizationSlug - Organization slug
147
+ * @param {Object} options - Query options
148
+ * @param {number} [options.limit=10] - Number of builds to fetch
149
+ * @param {string} [options.branch] - Filter by branch
150
+ * @returns {Promise<Array>} Array of builds
151
+ */
152
+ async getRecentBuilds(projectSlug, organizationSlug, options = {}) {
153
+ if (!this.apiService) {
154
+ // Return empty array if not authenticated
155
+ return [];
156
+ }
157
+ let queryParams = new globalThis.URLSearchParams();
158
+ if (options.limit) queryParams.append('limit', String(options.limit));
159
+ if (options.branch) queryParams.append('branch', options.branch);
160
+ let query = queryParams.toString();
161
+ let url = `/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/builds${query ? `?${query}` : ''}`;
162
+ try {
163
+ let response = await this.apiService.request(url, {
164
+ method: 'GET'
165
+ });
166
+ return response.builds || [];
167
+ } catch {
168
+ // Return empty array on error
169
+ return [];
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Create a project token
175
+ * @param {string} projectSlug - Project slug
176
+ * @param {string} organizationSlug - Organization slug
177
+ * @param {Object} tokenData - Token data
178
+ * @param {string} tokenData.name - Token name
179
+ * @param {string} [tokenData.description] - Token description
180
+ * @returns {Promise<Object>} Created token
181
+ */
182
+ async createProjectToken(projectSlug, organizationSlug, tokenData) {
183
+ if (!this.apiService) {
184
+ throw new VizzlyError('API service not available', 'NO_API_SERVICE');
185
+ }
186
+ try {
187
+ let response = await this.apiService.request(`/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/tokens`, {
188
+ method: 'POST',
189
+ headers: {
190
+ 'Content-Type': 'application/json'
191
+ },
192
+ body: JSON.stringify(tokenData)
193
+ });
194
+ return response.token;
195
+ } catch (error) {
196
+ throw new VizzlyError(`Failed to create project token: ${error.message}`, 'TOKEN_CREATE_FAILED', {
197
+ originalError: error
198
+ });
199
+ }
200
+ }
201
+
202
+ /**
203
+ * List project tokens
204
+ * @param {string} projectSlug - Project slug
205
+ * @param {string} organizationSlug - Organization slug
206
+ * @returns {Promise<Array>} Array of tokens
207
+ */
208
+ async listProjectTokens(projectSlug, organizationSlug) {
209
+ if (!this.apiService) {
210
+ throw new VizzlyError('API service not available', 'NO_API_SERVICE');
211
+ }
212
+ try {
213
+ let response = await this.apiService.request(`/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/tokens`, {
214
+ method: 'GET'
215
+ });
216
+ return response.tokens || [];
217
+ } catch (error) {
218
+ throw new VizzlyError(`Failed to fetch project tokens: ${error.message}`, 'TOKENS_FETCH_FAILED', {
219
+ originalError: error
220
+ });
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Revoke a project token
226
+ * @param {string} projectSlug - Project slug
227
+ * @param {string} organizationSlug - Organization slug
228
+ * @param {string} tokenId - Token ID
229
+ * @returns {Promise<void>}
230
+ */
231
+ async revokeProjectToken(projectSlug, organizationSlug, tokenId) {
232
+ if (!this.apiService) {
233
+ throw new VizzlyError('API service not available', 'NO_API_SERVICE');
234
+ }
235
+ try {
236
+ await this.apiService.request(`/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/tokens/${tokenId}`, {
237
+ method: 'DELETE'
238
+ });
239
+ } catch (error) {
240
+ throw new VizzlyError(`Failed to revoke project token: ${error.message}`, 'TOKEN_REVOKE_FAILED', {
241
+ originalError: error
242
+ });
243
+ }
244
+ }
245
+ }
@@ -8,12 +8,11 @@ import { createHttpServer } from '../server/http-server.js';
8
8
  import { createTddHandler } from '../server/handlers/tdd-handler.js';
9
9
  import { createApiHandler } from '../server/handlers/api-handler.js';
10
10
  export class ServerManager extends BaseService {
11
- constructor(config, logger) {
12
- super(config, {
13
- logger
14
- });
11
+ constructor(config, options = {}) {
12
+ super(config, options);
15
13
  this.httpServer = null;
16
14
  this.handler = null;
15
+ this.services = options.services || {};
17
16
  }
18
17
  async start(buildId = null, tddMode = false, setBaseline = false) {
19
18
  this.buildId = buildId;
@@ -30,7 +29,7 @@ export class ServerManager extends BaseService {
30
29
  const apiService = await this.createApiService();
31
30
  this.handler = createApiHandler(apiService);
32
31
  }
33
- this.httpServer = createHttpServer(port, this.handler);
32
+ this.httpServer = createHttpServer(port, this.handler, this.services);
34
33
  if (this.httpServer) {
35
34
  await this.httpServer.start();
36
35
  }