@vizzly-testing/cli 0.13.1 → 0.13.3
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/README.md +552 -88
- package/claude-plugin/.claude-plugin/README.md +4 -0
- package/claude-plugin/.mcp.json +4 -0
- package/claude-plugin/CHANGELOG.md +27 -0
- package/claude-plugin/mcp/vizzly-docs-server/README.md +95 -0
- package/claude-plugin/mcp/vizzly-docs-server/docs-fetcher.js +110 -0
- package/claude-plugin/mcp/vizzly-docs-server/index.js +283 -0
- package/claude-plugin/mcp/vizzly-server/cloud-api-provider.js +26 -10
- package/claude-plugin/mcp/vizzly-server/index.js +14 -1
- package/claude-plugin/mcp/vizzly-server/local-tdd-provider.js +61 -28
- package/dist/cli.js +4 -4
- package/dist/commands/run.js +1 -1
- package/dist/commands/tdd-daemon.js +54 -8
- package/dist/commands/tdd.js +8 -8
- package/dist/container/index.js +34 -3
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +29 -59
- package/dist/server/handlers/tdd-handler.js +18 -16
- package/dist/server/http-server.js +473 -4
- package/dist/services/config-service.js +371 -0
- package/dist/services/project-service.js +245 -0
- package/dist/services/server-manager.js +32 -5
- package/dist/services/static-report-generator.js +208 -0
- package/dist/services/tdd-service.js +14 -6
- package/dist/types/reporter/src/components/ui/form-field.d.ts +16 -0
- package/dist/types/reporter/src/components/views/projects-view.d.ts +1 -0
- package/dist/types/reporter/src/components/views/settings-view.d.ts +1 -0
- package/dist/types/reporter/src/hooks/use-auth.d.ts +10 -0
- package/dist/types/reporter/src/hooks/use-config.d.ts +9 -0
- package/dist/types/reporter/src/hooks/use-projects.d.ts +10 -0
- package/dist/types/reporter/src/services/api-client.d.ts +7 -0
- package/dist/types/server/http-server.d.ts +1 -1
- package/dist/types/services/config-service.d.ts +98 -0
- package/dist/types/services/project-service.d.ts +103 -0
- package/dist/types/services/server-manager.d.ts +2 -1
- package/dist/types/services/static-report-generator.d.ts +25 -0
- package/dist/types/services/tdd-service.d.ts +2 -2
- package/dist/utils/console-ui.js +26 -2
- package/docs/tdd-mode.md +31 -15
- 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
|
+
}
|
|
@@ -7,13 +7,14 @@ import { BaseService } from './base-service.js';
|
|
|
7
7
|
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
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
10
12
|
export class ServerManager extends BaseService {
|
|
11
|
-
constructor(config,
|
|
12
|
-
super(config,
|
|
13
|
-
logger
|
|
14
|
-
});
|
|
13
|
+
constructor(config, options = {}) {
|
|
14
|
+
super(config, options);
|
|
15
15
|
this.httpServer = null;
|
|
16
16
|
this.handler = null;
|
|
17
|
+
this.services = options.services || {};
|
|
17
18
|
}
|
|
18
19
|
async start(buildId = null, tddMode = false, setBaseline = false) {
|
|
19
20
|
this.buildId = buildId;
|
|
@@ -30,10 +31,36 @@ export class ServerManager extends BaseService {
|
|
|
30
31
|
const apiService = await this.createApiService();
|
|
31
32
|
this.handler = createApiHandler(apiService);
|
|
32
33
|
}
|
|
33
|
-
this.httpServer = createHttpServer(port, this.handler);
|
|
34
|
+
this.httpServer = createHttpServer(port, this.handler, this.services);
|
|
34
35
|
if (this.httpServer) {
|
|
35
36
|
await this.httpServer.start();
|
|
36
37
|
}
|
|
38
|
+
|
|
39
|
+
// Write server info to .vizzly/server.json for SDK discovery
|
|
40
|
+
// This allows SDKs that can't access environment variables (like Swift/iOS)
|
|
41
|
+
// to discover both the server port and current build ID
|
|
42
|
+
try {
|
|
43
|
+
const vizzlyDir = join(process.cwd(), '.vizzly');
|
|
44
|
+
mkdirSync(vizzlyDir, {
|
|
45
|
+
recursive: true
|
|
46
|
+
});
|
|
47
|
+
const serverFile = join(vizzlyDir, 'server.json');
|
|
48
|
+
const serverInfo = {
|
|
49
|
+
port: port.toString(),
|
|
50
|
+
pid: process.pid,
|
|
51
|
+
startTime: Date.now()
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Include buildId if we have one (for `vizzly run` mode)
|
|
55
|
+
if (this.buildId) {
|
|
56
|
+
serverInfo.buildId = this.buildId;
|
|
57
|
+
}
|
|
58
|
+
writeFileSync(serverFile, JSON.stringify(serverInfo, null, 2));
|
|
59
|
+
this.logger.debug(`Wrote server info to ${serverFile}`);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Non-fatal - SDK can still use health check or environment variables
|
|
62
|
+
this.logger.debug(`Failed to write server.json: ${error.message}`);
|
|
63
|
+
}
|
|
37
64
|
}
|
|
38
65
|
async createApiService() {
|
|
39
66
|
if (!this.config.apiKey) return null;
|