@vizzly-testing/cli 0.20.1-beta.0 → 0.20.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/README.md +16 -18
- package/dist/cli.js +177 -2
- package/dist/client/index.js +144 -77
- package/dist/commands/doctor.js +118 -33
- package/dist/commands/finalize.js +8 -3
- package/dist/commands/init.js +13 -18
- package/dist/commands/login.js +42 -49
- package/dist/commands/logout.js +13 -5
- package/dist/commands/project.js +95 -67
- package/dist/commands/run.js +32 -6
- package/dist/commands/status.js +81 -50
- package/dist/commands/tdd-daemon.js +61 -32
- package/dist/commands/tdd.js +14 -26
- package/dist/commands/upload.js +18 -9
- package/dist/commands/whoami.js +40 -38
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +204 -22
- package/dist/server/handlers/tdd-handler.js +113 -7
- package/dist/server/http-server.js +9 -3
- package/dist/server/routers/baseline.js +58 -0
- package/dist/server/routers/dashboard.js +10 -6
- package/dist/server/routers/screenshot.js +32 -0
- package/dist/server-manager/core.js +5 -2
- package/dist/server-manager/operations.js +2 -1
- package/dist/services/config-service.js +306 -0
- package/dist/tdd/tdd-service.js +190 -126
- package/dist/types/client.d.ts +25 -2
- package/dist/utils/colors.js +187 -39
- package/dist/utils/config-loader.js +3 -6
- package/dist/utils/context.js +228 -0
- package/dist/utils/output.js +449 -14
- package/docs/api-reference.md +173 -8
- package/docs/tui-elements.md +560 -0
- package/package.json +13 -7
- package/dist/report-generator/core.js +0 -315
- package/dist/report-generator/index.js +0 -8
- package/dist/report-generator/operations.js +0 -196
- package/dist/services/static-report-generator.js +0 -65
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Service
|
|
3
|
+
* Manages configuration for the TDD dashboard settings page
|
|
4
|
+
*
|
|
5
|
+
* Provides read/write access to:
|
|
6
|
+
* - Merged config (read-only combination of all sources)
|
|
7
|
+
* - Project config (vizzly.config.js in working directory)
|
|
8
|
+
* - Global config (~/.vizzly/config.json)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { writeFile } from 'node:fs/promises';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { cosmiconfigSync } from 'cosmiconfig';
|
|
15
|
+
import { loadGlobalConfig, saveGlobalConfig } from '../utils/global-config.js';
|
|
16
|
+
import * as output from '../utils/output.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default configuration values
|
|
20
|
+
*/
|
|
21
|
+
let DEFAULT_CONFIG = {
|
|
22
|
+
comparison: {
|
|
23
|
+
threshold: 2.0
|
|
24
|
+
},
|
|
25
|
+
server: {
|
|
26
|
+
port: 47392,
|
|
27
|
+
timeout: 30000
|
|
28
|
+
},
|
|
29
|
+
build: {
|
|
30
|
+
name: 'Build {timestamp}',
|
|
31
|
+
environment: 'test'
|
|
32
|
+
},
|
|
33
|
+
tdd: {
|
|
34
|
+
openReport: false
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a config service instance
|
|
40
|
+
* @param {Object} options
|
|
41
|
+
* @param {string} options.workingDir - Working directory for project config
|
|
42
|
+
* @returns {Object} Config service with getConfig, updateConfig, validateConfig methods
|
|
43
|
+
*/
|
|
44
|
+
export function createConfigService({
|
|
45
|
+
workingDir
|
|
46
|
+
}) {
|
|
47
|
+
let projectConfigPath = null;
|
|
48
|
+
let projectConfigFormat = 'js'; // 'js' or 'json'
|
|
49
|
+
|
|
50
|
+
// Find project config file
|
|
51
|
+
let explorer = cosmiconfigSync('vizzly');
|
|
52
|
+
let searchResult = explorer.search(workingDir);
|
|
53
|
+
if (searchResult?.filepath) {
|
|
54
|
+
projectConfigPath = searchResult.filepath;
|
|
55
|
+
projectConfigFormat = searchResult.filepath.endsWith('.json') ? 'json' : 'js';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get configuration by type
|
|
60
|
+
* @param {'merged'|'project'|'global'} type
|
|
61
|
+
* @returns {Promise<Object>}
|
|
62
|
+
*/
|
|
63
|
+
async function getConfig(type) {
|
|
64
|
+
if (type === 'merged') {
|
|
65
|
+
return getMergedConfig();
|
|
66
|
+
} else if (type === 'project') {
|
|
67
|
+
return getProjectConfig();
|
|
68
|
+
} else if (type === 'global') {
|
|
69
|
+
return getGlobalConfigData();
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`Unknown config type: ${type}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get merged configuration with source tracking
|
|
76
|
+
*/
|
|
77
|
+
async function getMergedConfig() {
|
|
78
|
+
let config = {
|
|
79
|
+
...DEFAULT_CONFIG
|
|
80
|
+
};
|
|
81
|
+
let sources = {};
|
|
82
|
+
|
|
83
|
+
// Layer 1: Global config
|
|
84
|
+
let globalConfig = await loadGlobalConfig();
|
|
85
|
+
if (globalConfig.settings) {
|
|
86
|
+
mergeWithTracking(config, globalConfig.settings, sources, 'global');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Layer 2: Project config
|
|
90
|
+
if (projectConfigPath && existsSync(projectConfigPath)) {
|
|
91
|
+
try {
|
|
92
|
+
let result = explorer.load(projectConfigPath);
|
|
93
|
+
let projectConfig = result?.config?.default || result?.config || {};
|
|
94
|
+
mergeWithTracking(config, projectConfig, sources, 'project');
|
|
95
|
+
} catch (error) {
|
|
96
|
+
output.debug('config-service', `Error loading project config: ${error.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Layer 3: Environment variables
|
|
101
|
+
if (process.env.VIZZLY_THRESHOLD) {
|
|
102
|
+
config.comparison.threshold = parseFloat(process.env.VIZZLY_THRESHOLD);
|
|
103
|
+
sources.comparison = 'env';
|
|
104
|
+
}
|
|
105
|
+
if (process.env.VIZZLY_PORT) {
|
|
106
|
+
config.server.port = parseInt(process.env.VIZZLY_PORT, 10);
|
|
107
|
+
sources.server = 'env';
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
config,
|
|
111
|
+
sources
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get project-level configuration only
|
|
117
|
+
*/
|
|
118
|
+
async function getProjectConfig() {
|
|
119
|
+
if (!projectConfigPath || !existsSync(projectConfigPath)) {
|
|
120
|
+
return {
|
|
121
|
+
config: {},
|
|
122
|
+
path: null
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
let result = explorer.load(projectConfigPath);
|
|
127
|
+
let config = result?.config?.default || result?.config || {};
|
|
128
|
+
return {
|
|
129
|
+
config,
|
|
130
|
+
path: projectConfigPath
|
|
131
|
+
};
|
|
132
|
+
} catch (error) {
|
|
133
|
+
output.debug('config-service', `Error loading project config: ${error.message}`);
|
|
134
|
+
return {
|
|
135
|
+
config: {},
|
|
136
|
+
path: projectConfigPath,
|
|
137
|
+
error: error.message
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get global configuration only
|
|
144
|
+
*/
|
|
145
|
+
async function getGlobalConfigData() {
|
|
146
|
+
let globalConfig = await loadGlobalConfig();
|
|
147
|
+
return {
|
|
148
|
+
config: globalConfig.settings || {},
|
|
149
|
+
path: join(process.env.VIZZLY_HOME || join(process.env.HOME || '', '.vizzly'), 'config.json')
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Update configuration by type
|
|
155
|
+
* @param {'project'|'global'} type
|
|
156
|
+
* @param {Object} updates - Config updates to apply
|
|
157
|
+
* @returns {Promise<Object>}
|
|
158
|
+
*/
|
|
159
|
+
async function updateConfig(type, updates) {
|
|
160
|
+
if (type === 'project') {
|
|
161
|
+
return updateProjectConfig(updates);
|
|
162
|
+
} else if (type === 'global') {
|
|
163
|
+
return updateGlobalConfig(updates);
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`Cannot update config type: ${type}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Update project configuration (vizzly.config.js)
|
|
170
|
+
*/
|
|
171
|
+
async function updateProjectConfig(updates) {
|
|
172
|
+
// If no project config exists, create one
|
|
173
|
+
if (!projectConfigPath) {
|
|
174
|
+
projectConfigPath = join(workingDir, 'vizzly.config.js');
|
|
175
|
+
projectConfigFormat = 'js';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Read existing config
|
|
179
|
+
let existingConfig = {};
|
|
180
|
+
if (existsSync(projectConfigPath)) {
|
|
181
|
+
try {
|
|
182
|
+
let result = explorer.load(projectConfigPath);
|
|
183
|
+
existingConfig = result?.config?.default || result?.config || {};
|
|
184
|
+
} catch {
|
|
185
|
+
// Start fresh if corrupted
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Merge updates
|
|
190
|
+
let newConfig = mergeDeep(existingConfig, updates);
|
|
191
|
+
|
|
192
|
+
// Write based on format
|
|
193
|
+
if (projectConfigFormat === 'json') {
|
|
194
|
+
await writeFile(projectConfigPath, JSON.stringify(newConfig, null, 2));
|
|
195
|
+
} else {
|
|
196
|
+
// Write as ES module
|
|
197
|
+
let content = `import { defineConfig } from '@vizzly-testing/cli/config';
|
|
198
|
+
|
|
199
|
+
export default defineConfig(${JSON.stringify(newConfig, null, 2)});
|
|
200
|
+
`;
|
|
201
|
+
await writeFile(projectConfigPath, content);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Clear cosmiconfig cache so next read gets fresh data
|
|
205
|
+
explorer.clearCaches();
|
|
206
|
+
return {
|
|
207
|
+
success: true,
|
|
208
|
+
path: projectConfigPath
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Update global configuration (~/.vizzly/config.json)
|
|
214
|
+
*/
|
|
215
|
+
async function updateGlobalConfig(updates) {
|
|
216
|
+
let globalConfig = await loadGlobalConfig();
|
|
217
|
+
if (!globalConfig.settings) {
|
|
218
|
+
globalConfig.settings = {};
|
|
219
|
+
}
|
|
220
|
+
globalConfig.settings = mergeDeep(globalConfig.settings, updates);
|
|
221
|
+
await saveGlobalConfig(globalConfig);
|
|
222
|
+
return {
|
|
223
|
+
success: true
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Validate configuration
|
|
229
|
+
* @param {Object} config - Config to validate
|
|
230
|
+
* @returns {Promise<Object>}
|
|
231
|
+
*/
|
|
232
|
+
async function validateConfig(config) {
|
|
233
|
+
let errors = [];
|
|
234
|
+
let warnings = [];
|
|
235
|
+
|
|
236
|
+
// Validate threshold
|
|
237
|
+
if (config.comparison?.threshold !== undefined) {
|
|
238
|
+
let threshold = config.comparison.threshold;
|
|
239
|
+
if (typeof threshold !== 'number' || threshold < 0) {
|
|
240
|
+
errors.push('comparison.threshold must be a non-negative number');
|
|
241
|
+
} else if (threshold > 100) {
|
|
242
|
+
warnings.push('comparison.threshold above 100 may cause all comparisons to pass');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Validate port
|
|
247
|
+
if (config.server?.port !== undefined) {
|
|
248
|
+
let port = config.server.port;
|
|
249
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
250
|
+
errors.push('server.port must be an integer between 1 and 65535');
|
|
251
|
+
} else if (port < 1024) {
|
|
252
|
+
warnings.push('server.port below 1024 may require elevated privileges');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Validate timeout
|
|
257
|
+
if (config.server?.timeout !== undefined) {
|
|
258
|
+
let timeout = config.server.timeout;
|
|
259
|
+
if (!Number.isInteger(timeout) || timeout < 0) {
|
|
260
|
+
errors.push('server.timeout must be a non-negative integer');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
valid: errors.length === 0,
|
|
265
|
+
errors,
|
|
266
|
+
warnings
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
getConfig,
|
|
271
|
+
updateConfig,
|
|
272
|
+
validateConfig
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Deep merge two objects
|
|
278
|
+
*/
|
|
279
|
+
function mergeDeep(target, source) {
|
|
280
|
+
let result = {
|
|
281
|
+
...target
|
|
282
|
+
};
|
|
283
|
+
for (let key in source) {
|
|
284
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
285
|
+
result[key] = mergeDeep(result[key] || {}, source[key]);
|
|
286
|
+
} else {
|
|
287
|
+
result[key] = source[key];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Merge config with source tracking
|
|
295
|
+
*/
|
|
296
|
+
function mergeWithTracking(target, source, sources, sourceName) {
|
|
297
|
+
for (let key in source) {
|
|
298
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
299
|
+
if (!target[key]) target[key] = {};
|
|
300
|
+
mergeWithTracking(target[key], source[key], sources, sourceName);
|
|
301
|
+
} else {
|
|
302
|
+
target[key] = source[key];
|
|
303
|
+
sources[key] = sourceName;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|