@vizzly-testing/cli 0.30.0 → 0.31.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.
- package/README.md +9 -9
- package/dist/api/client.js +6 -3
- package/dist/cli.js +58 -29
- package/dist/client/index.js +21 -29
- package/dist/commands/api.js +145 -90
- package/dist/commands/baselines.js +78 -47
- package/dist/commands/builds.js +9 -8
- package/dist/commands/comparisons.js +7 -8
- package/dist/commands/config-cmd.js +15 -7
- package/dist/commands/context.js +329 -18
- package/dist/commands/doctor.js +116 -81
- package/dist/commands/init.js +202 -201
- package/dist/commands/login.js +134 -65
- package/dist/commands/logout.js +17 -6
- package/dist/commands/preview.js +29 -57
- package/dist/commands/project.js +120 -0
- package/dist/commands/projects.js +9 -2
- package/dist/commands/review.js +236 -202
- package/dist/commands/run.js +75 -72
- package/dist/commands/status.js +228 -150
- package/dist/commands/tdd-daemon-process.js +98 -0
- package/dist/commands/tdd-daemon.js +318 -212
- package/dist/commands/tdd.js +23 -8
- package/dist/commands/upload.js +10 -9
- package/dist/commands/whoami.js +63 -25
- package/dist/config/core.js +36 -35
- package/dist/context/local-workspace-provider.js +132 -12
- package/dist/context/provider-resolver.js +5 -1
- package/dist/index.js +5 -4
- package/dist/plugin-loader.js +55 -49
- package/dist/project/core.js +2 -2
- package/dist/project/operations.js +6 -3
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +7 -7
- package/dist/sdk/index.js +162 -107
- package/dist/server/handlers/tdd-handler.js +30 -54
- package/dist/server/routers/baseline.js +195 -178
- package/dist/server/routers/events.js +212 -174
- package/dist/server/routers/projects.js +14 -5
- package/dist/server/routers/screenshot.js +1 -31
- package/dist/services/build-manager.js +19 -97
- package/dist/services/config-service.js +26 -34
- package/dist/services/index.js +2 -4
- package/dist/services/static-report-generator.js +11 -12
- package/dist/services/test-runner.js +21 -27
- package/dist/tdd/index.js +1 -1
- package/dist/tdd/metadata/region-metadata.js +0 -37
- package/dist/tdd/runtime-context.js +78 -0
- package/dist/tdd/server-registry.js +38 -17
- package/dist/tdd/services/baseline-manager.js +23 -0
- package/dist/tdd/services/hotspot-service.js +0 -13
- package/dist/tdd/tdd-service.js +82 -113
- package/dist/types/client.d.ts +36 -1
- package/dist/types/index.d.ts +114 -22
- package/dist/types/sdk.d.ts +40 -0
- package/dist/uploader/core.js +5 -4
- package/dist/uploader/index.js +79 -2
- package/dist/uploader/operations.js +40 -5
- package/dist/utils/api-url.js +14 -0
- package/dist/utils/async-utils.js +26 -0
- package/dist/utils/ci-env.js +19 -12
- package/dist/utils/config-loader.js +39 -65
- package/dist/utils/config-schema.js +19 -55
- package/dist/utils/fetch-utils.js +12 -5
- package/dist/utils/git.js +35 -72
- package/dist/utils/global-config.js +20 -1
- package/dist/utils/output.js +19 -7
- package/dist/utils/patterns.js +14 -0
- package/dist/utils/project-link-store.js +105 -0
- package/dist/utils/screenshot-options.js +28 -0
- package/dist/utils/secret-store.js +65 -0
- package/package.json +11 -8
- package/dist/services/screenshot-server.js +0 -65
- package/dist/services/server-manager.js +0 -81
- package/dist/services/uploader.js +0 -100
- package/dist/tdd/services/region-service.js +0 -75
- package/dist/utils/build-history.js +0 -103
package/dist/commands/doctor.js
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
import { URL } from 'node:url';
|
|
2
|
-
import { createApiClient, getBuilds } from '../api/index.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* Doctor command implementation - Run diagnostics to check environment
|
|
11
|
-
* @param {Object} options - Command options
|
|
12
|
-
* @param {Object} globalOptions - Global CLI options
|
|
13
|
-
*/
|
|
14
|
-
export async function doctorCommand(options = {}, globalOptions = {}) {
|
|
15
|
-
output.configure({
|
|
16
|
-
json: globalOptions.json,
|
|
17
|
-
verbose: globalOptions.verbose,
|
|
18
|
-
color: !globalOptions.noColor
|
|
19
|
-
});
|
|
20
|
-
let diagnostics = {
|
|
2
|
+
import { createApiClient as defaultCreateApiClient, getBuilds as defaultGetBuilds } from '../api/index.js';
|
|
3
|
+
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
4
|
+
import { getContext as defaultGetContext } from '../utils/context.js';
|
|
5
|
+
import { getApiToken as defaultGetApiToken } from '../utils/environment-config.js';
|
|
6
|
+
import * as defaultOutput from '../utils/output.js';
|
|
7
|
+
export let MIN_NODE_MAJOR = 22;
|
|
8
|
+
export function createDoctorDiagnostics() {
|
|
9
|
+
return {
|
|
21
10
|
environment: {
|
|
22
11
|
nodeVersion: null,
|
|
23
12
|
nodeVersionValid: null
|
|
@@ -35,79 +24,126 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
|
|
|
35
24
|
error: null
|
|
36
25
|
}
|
|
37
26
|
};
|
|
27
|
+
}
|
|
28
|
+
export function getNodeVersionCheck(nodeVersion = process.version, minMajor = MIN_NODE_MAJOR) {
|
|
29
|
+
let nodeMajor = parseNodeMajorVersion(nodeVersion);
|
|
30
|
+
let ok = nodeMajor !== null && nodeMajor >= minMajor;
|
|
31
|
+
let value = ok ? `${nodeVersion} (supported)` : nodeMajor === null ? `${nodeVersion} (unrecognized Node.js version)` : `${nodeVersion} (requires >= ${minMajor})`;
|
|
32
|
+
return {
|
|
33
|
+
diagnostic: {
|
|
34
|
+
nodeVersion,
|
|
35
|
+
nodeVersionValid: ok
|
|
36
|
+
},
|
|
37
|
+
check: {
|
|
38
|
+
name: 'Node.js',
|
|
39
|
+
value,
|
|
40
|
+
ok
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function parseNodeMajorVersion(nodeVersion) {
|
|
45
|
+
let match = /^v?(\d+)\.\d+\.\d+$/.exec(nodeVersion);
|
|
46
|
+
return match ? Number(match[1]) : null;
|
|
47
|
+
}
|
|
48
|
+
export function getApiUrlCheck(apiUrl) {
|
|
49
|
+
try {
|
|
50
|
+
let url = new URL(apiUrl);
|
|
51
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
52
|
+
throw new Error('URL must use http or https');
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
apiUrl,
|
|
56
|
+
apiUrlValid: true,
|
|
57
|
+
check: {
|
|
58
|
+
name: 'API URL',
|
|
59
|
+
value: apiUrl,
|
|
60
|
+
ok: true
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
} catch {
|
|
64
|
+
return {
|
|
65
|
+
apiUrl,
|
|
66
|
+
apiUrlValid: false,
|
|
67
|
+
check: {
|
|
68
|
+
name: 'API URL',
|
|
69
|
+
value: 'invalid (check VIZZLY_API_URL)',
|
|
70
|
+
ok: false
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function getThresholdCheck(thresholdValue) {
|
|
76
|
+
let threshold = Number(thresholdValue);
|
|
77
|
+
// CIEDE2000 threshold: 0 = exact, 1 = JND, 2 = recommended, 3+ = permissive
|
|
78
|
+
let thresholdValid = Number.isFinite(threshold) && threshold >= 0;
|
|
79
|
+
return {
|
|
80
|
+
threshold,
|
|
81
|
+
thresholdValid,
|
|
82
|
+
check: thresholdValid ? {
|
|
83
|
+
name: 'Threshold',
|
|
84
|
+
value: `${threshold} (CIEDE2000)`,
|
|
85
|
+
ok: true
|
|
86
|
+
} : {
|
|
87
|
+
name: 'Threshold',
|
|
88
|
+
value: 'invalid',
|
|
89
|
+
ok: false
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Doctor command implementation - Run diagnostics to check environment
|
|
96
|
+
* @param {Object} options - Command options
|
|
97
|
+
* @param {Object} globalOptions - Global CLI options
|
|
98
|
+
* @param {Object} deps - Dependencies for testing
|
|
99
|
+
*/
|
|
100
|
+
export async function doctorCommand(options = {}, globalOptions = {}, deps = {}) {
|
|
101
|
+
let {
|
|
102
|
+
createApiClient = defaultCreateApiClient,
|
|
103
|
+
getApiToken = defaultGetApiToken,
|
|
104
|
+
getBuilds = defaultGetBuilds,
|
|
105
|
+
getContext = defaultGetContext,
|
|
106
|
+
loadConfig = defaultLoadConfig,
|
|
107
|
+
nodeVersion = process.version,
|
|
108
|
+
output = defaultOutput,
|
|
109
|
+
exit = code => process.exit(code)
|
|
110
|
+
} = deps;
|
|
111
|
+
output.configure({
|
|
112
|
+
json: globalOptions.json,
|
|
113
|
+
verbose: globalOptions.verbose,
|
|
114
|
+
color: !globalOptions.noColor
|
|
115
|
+
});
|
|
116
|
+
let diagnostics = createDoctorDiagnostics();
|
|
38
117
|
let hasErrors = false;
|
|
39
118
|
let checks = [];
|
|
40
119
|
try {
|
|
41
120
|
// Determine if we'll attempt remote checks (API connectivity)
|
|
42
|
-
let
|
|
121
|
+
let hasApiToken = Boolean(getApiToken());
|
|
122
|
+
let willCheckConnectivity = Boolean(options.api || hasApiToken);
|
|
43
123
|
|
|
44
124
|
// Show header
|
|
45
125
|
output.header('doctor', willCheckConnectivity ? 'full' : 'local');
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
diagnostics.environment.nodeVersion = nodeVersion;
|
|
51
|
-
diagnostics.environment.nodeVersionValid = nodeMajor >= 20;
|
|
52
|
-
if (nodeMajor >= 20) {
|
|
53
|
-
checks.push({
|
|
54
|
-
name: 'Node.js',
|
|
55
|
-
value: `${nodeVersion} (supported)`,
|
|
56
|
-
ok: true
|
|
57
|
-
});
|
|
58
|
-
} else {
|
|
59
|
-
checks.push({
|
|
60
|
-
name: 'Node.js',
|
|
61
|
-
value: `${nodeVersion} (requires >= 20)`,
|
|
62
|
-
ok: false
|
|
63
|
-
});
|
|
126
|
+
let nodeCheck = getNodeVersionCheck(nodeVersion);
|
|
127
|
+
diagnostics.environment = nodeCheck.diagnostic;
|
|
128
|
+
checks.push(nodeCheck.check);
|
|
129
|
+
if (!nodeCheck.check.ok) {
|
|
64
130
|
hasErrors = true;
|
|
65
131
|
}
|
|
66
132
|
|
|
67
133
|
// Load configuration (apply global CLI overrides like --config only)
|
|
68
134
|
let config = await loadConfig(globalOptions.config);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
diagnostics.configuration.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
75
|
-
throw new ConfigError('URL must use http or https');
|
|
76
|
-
}
|
|
77
|
-
diagnostics.configuration.apiUrlValid = true;
|
|
78
|
-
checks.push({
|
|
79
|
-
name: 'API URL',
|
|
80
|
-
value: config.apiUrl,
|
|
81
|
-
ok: true
|
|
82
|
-
});
|
|
83
|
-
} catch (_e) {
|
|
84
|
-
diagnostics.configuration.apiUrlValid = false;
|
|
85
|
-
checks.push({
|
|
86
|
-
name: 'API URL',
|
|
87
|
-
value: 'invalid (check VIZZLY_API_URL)',
|
|
88
|
-
ok: false
|
|
89
|
-
});
|
|
135
|
+
let apiUrlCheck = getApiUrlCheck(config.apiUrl);
|
|
136
|
+
diagnostics.configuration.apiUrl = apiUrlCheck.apiUrl;
|
|
137
|
+
diagnostics.configuration.apiUrlValid = apiUrlCheck.apiUrlValid;
|
|
138
|
+
checks.push(apiUrlCheck.check);
|
|
139
|
+
if (!apiUrlCheck.check.ok) {
|
|
90
140
|
hasErrors = true;
|
|
91
141
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
let thresholdValid = Number.isFinite(threshold) && threshold >= 0;
|
|
98
|
-
diagnostics.configuration.thresholdValid = thresholdValid;
|
|
99
|
-
if (thresholdValid) {
|
|
100
|
-
checks.push({
|
|
101
|
-
name: 'Threshold',
|
|
102
|
-
value: `${threshold} (CIEDE2000)`,
|
|
103
|
-
ok: true
|
|
104
|
-
});
|
|
105
|
-
} else {
|
|
106
|
-
checks.push({
|
|
107
|
-
name: 'Threshold',
|
|
108
|
-
value: 'invalid',
|
|
109
|
-
ok: false
|
|
110
|
-
});
|
|
142
|
+
let thresholdCheck = getThresholdCheck(config?.comparison?.threshold);
|
|
143
|
+
diagnostics.configuration.threshold = thresholdCheck.threshold;
|
|
144
|
+
diagnostics.configuration.thresholdValid = thresholdCheck.thresholdValid;
|
|
145
|
+
checks.push(thresholdCheck.check);
|
|
146
|
+
if (!thresholdCheck.check.ok) {
|
|
111
147
|
hasErrors = true;
|
|
112
148
|
}
|
|
113
149
|
|
|
@@ -121,8 +157,7 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
|
|
|
121
157
|
});
|
|
122
158
|
|
|
123
159
|
// Optional: API connectivity check when --api is provided or VIZZLY_TOKEN is present
|
|
124
|
-
|
|
125
|
-
if (options.api || autoApi) {
|
|
160
|
+
if (willCheckConnectivity) {
|
|
126
161
|
diagnostics.connectivity.checked = true;
|
|
127
162
|
if (!config.apiKey) {
|
|
128
163
|
diagnostics.connectivity.ok = false;
|
|
@@ -227,7 +262,7 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
|
|
|
227
262
|
output.error('Failed to run preflight', error);
|
|
228
263
|
} finally {
|
|
229
264
|
output.cleanup();
|
|
230
|
-
if (hasErrors)
|
|
265
|
+
if (hasErrors) exit(1);
|
|
231
266
|
}
|
|
232
267
|
}
|
|
233
268
|
|
package/dist/commands/init.js
CHANGED
|
@@ -6,79 +6,111 @@ import { VizzlyError } from '../errors/vizzly-error.js';
|
|
|
6
6
|
import { loadPlugins } from '../plugin-loader.js';
|
|
7
7
|
import { loadConfig } from '../utils/config-loader.js';
|
|
8
8
|
import * as output from '../utils/output.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
let configValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(configValueSchema), z.record(z.string(), configValueSchema)]));
|
|
10
|
+
let configSchemaValidator = z.record(z.string(), configValueSchema);
|
|
11
|
+
function createInitDeps(deps = {}) {
|
|
12
|
+
return {
|
|
13
|
+
access: deps.access || fs.access,
|
|
14
|
+
cwd: deps.cwd || (() => process.cwd()),
|
|
15
|
+
loadConfig: deps.loadConfig || loadConfig,
|
|
16
|
+
loadPlugins: deps.loadPlugins || loadPlugins,
|
|
17
|
+
output: deps.output || output,
|
|
18
|
+
writeFile: deps.writeFile || fs.writeFile
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function configureOutput(output, options) {
|
|
22
|
+
output.configure({
|
|
23
|
+
json: options.json || false,
|
|
24
|
+
verbose: options.verbose || false,
|
|
25
|
+
color: options.color !== false
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function getInitConfigPath(cwd = process.cwd()) {
|
|
29
|
+
return path.join(cwd, 'vizzly.config.js');
|
|
30
|
+
}
|
|
31
|
+
export async function fileExists(filePath, access = fs.access) {
|
|
32
|
+
try {
|
|
33
|
+
await access(filePath);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
16
37
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
}
|
|
39
|
+
export function getPluginsWithConfig(plugins = []) {
|
|
40
|
+
return plugins.filter(plugin => plugin.configSchema);
|
|
41
|
+
}
|
|
42
|
+
export function getPluginConfigNames(plugins = []) {
|
|
43
|
+
return getPluginsWithConfig(plugins).map(plugin => plugin.name);
|
|
44
|
+
}
|
|
45
|
+
function formatObjectKey(key) {
|
|
46
|
+
if (/^[A-Za-z_$][\w$]*$/.test(key)) {
|
|
47
|
+
return key;
|
|
48
|
+
}
|
|
49
|
+
return formatValue(key);
|
|
50
|
+
}
|
|
51
|
+
export function formatValue(value, depth = 0) {
|
|
52
|
+
let indent = ' '.repeat(depth);
|
|
53
|
+
let nextIndent = ' '.repeat(depth + 1);
|
|
54
|
+
if (value === null) return 'null';
|
|
55
|
+
if (value === undefined) return 'undefined';
|
|
56
|
+
if (typeof value === 'string') {
|
|
57
|
+
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n')}'`;
|
|
58
|
+
}
|
|
59
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
60
|
+
return String(value);
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
if (value.length === 0) return '[]';
|
|
64
|
+
let items = value.map(item => {
|
|
65
|
+
let formatted = formatValue(item, depth + 1);
|
|
66
|
+
return `${nextIndent}${formatted}`;
|
|
67
|
+
});
|
|
68
|
+
return `[\n${items.join(',\n')}\n${indent}]`;
|
|
69
|
+
}
|
|
70
|
+
if (typeof value === 'object') {
|
|
71
|
+
let entries = Object.entries(value);
|
|
72
|
+
if (entries.length === 0) return '{}';
|
|
73
|
+
let props = entries.map(([key, entryValue]) => {
|
|
74
|
+
let formatted = formatValue(entryValue, depth + 1);
|
|
75
|
+
return `${nextIndent}${formatObjectKey(key)}: ${formatted}`;
|
|
76
|
+
});
|
|
77
|
+
return `{\n${props.join(',\n')}\n${indent}}`;
|
|
78
|
+
}
|
|
79
|
+
return String(value);
|
|
80
|
+
}
|
|
81
|
+
export function formatPluginConfig(plugin, output = null) {
|
|
82
|
+
try {
|
|
83
|
+
configSchemaValidator.parse(plugin.configSchema);
|
|
84
|
+
let configEntries = [];
|
|
85
|
+
for (let [key, value] of Object.entries(plugin.configSchema)) {
|
|
86
|
+
let formattedValue = formatValue(value, 1);
|
|
87
|
+
configEntries.push(` // ${plugin.name} plugin configuration\n ${formatObjectKey(key)}: ${formattedValue}`);
|
|
42
88
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
plugins: pluginNames
|
|
62
|
-
};
|
|
89
|
+
return configEntries.join(',\n\n');
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof z.ZodError) {
|
|
92
|
+
let messages = error.issues.map(zodError => `${zodError.path.join('.')}: ${zodError.message}`);
|
|
93
|
+
output?.warn(`Invalid config schema for plugin ${plugin.name}: ${messages.join(', ')}`);
|
|
94
|
+
} else {
|
|
95
|
+
output?.warn(`Failed to format config for plugin ${plugin.name}: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function generatePluginConfigs(plugins = [], output = null) {
|
|
101
|
+
let sections = [];
|
|
102
|
+
for (let plugin of plugins) {
|
|
103
|
+
if (plugin.configSchema) {
|
|
104
|
+
let configStr = formatPluginConfig(plugin, output);
|
|
105
|
+
if (configStr) {
|
|
106
|
+
sections.push(configStr);
|
|
63
107
|
}
|
|
64
|
-
|
|
65
|
-
// Show next steps
|
|
66
|
-
this.showNextSteps();
|
|
67
|
-
output.blank();
|
|
68
|
-
output.complete('Vizzly CLI setup complete');
|
|
69
|
-
return {
|
|
70
|
-
status: 'created',
|
|
71
|
-
configPath,
|
|
72
|
-
plugins: pluginNames
|
|
73
|
-
};
|
|
74
|
-
} catch (error) {
|
|
75
|
-
throw new VizzlyError('Failed to initialize Vizzly configuration', 'INIT_FAILED', {
|
|
76
|
-
error: error.message
|
|
77
|
-
});
|
|
78
108
|
}
|
|
79
109
|
}
|
|
80
|
-
|
|
81
|
-
|
|
110
|
+
return sections.length > 0 ? sections.join(',\n\n') : '';
|
|
111
|
+
}
|
|
112
|
+
export function createConfigContent(plugins = [], output = null) {
|
|
113
|
+
let coreConfig = `export default {
|
|
82
114
|
// Server configuration (for run command)
|
|
83
115
|
server: {
|
|
84
116
|
port: 47392,
|
|
@@ -100,155 +132,124 @@ export class InitCommand {
|
|
|
100
132
|
|
|
101
133
|
// Comparison configuration (CIEDE2000 Delta E: 0=exact, 1=JND, 2=recommended)
|
|
102
134
|
comparison: {
|
|
103
|
-
threshold: 2.0
|
|
135
|
+
threshold: 2.0,
|
|
136
|
+
minClusterSize: 2
|
|
104
137
|
},
|
|
105
138
|
|
|
106
139
|
// TDD configuration
|
|
107
140
|
tdd: {
|
|
108
141
|
openReport: false // Whether to auto-open HTML report in browser
|
|
109
142
|
}`;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (pluginConfigs) {
|
|
114
|
-
coreConfig += `,\n\n${pluginConfigs}`;
|
|
115
|
-
}
|
|
116
|
-
coreConfig += '\n};\n';
|
|
117
|
-
await fs.writeFile(configPath, coreConfig, 'utf8');
|
|
118
|
-
|
|
119
|
-
// Skip human-readable output in JSON mode
|
|
120
|
-
if (options.json) return;
|
|
121
|
-
output.header('init');
|
|
122
|
-
output.complete('Created vizzly.config.js');
|
|
123
|
-
|
|
124
|
-
// Log discovered plugins
|
|
125
|
-
let pluginsWithConfig = this.plugins.filter(p => p.configSchema);
|
|
126
|
-
if (pluginsWithConfig.length > 0) {
|
|
127
|
-
output.hint(`Added config for ${pluginsWithConfig.length} plugin(s):`);
|
|
128
|
-
output.list(pluginsWithConfig.map(p => p.name), {
|
|
129
|
-
indent: 4
|
|
130
|
-
});
|
|
131
|
-
}
|
|
143
|
+
let pluginConfigs = generatePluginConfigs(plugins, output);
|
|
144
|
+
if (pluginConfigs) {
|
|
145
|
+
coreConfig += `,\n\n${pluginConfigs}`;
|
|
132
146
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
sections.push(configStr);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return sections.length > 0 ? sections.join(',\n\n') : '';
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Format a plugin's config schema as JavaScript code
|
|
153
|
-
* @param {Object} plugin - Plugin with configSchema
|
|
154
|
-
* @returns {string} Formatted config string
|
|
155
|
-
*/
|
|
156
|
-
formatPluginConfig(plugin) {
|
|
157
|
-
try {
|
|
158
|
-
// Validate config schema structure with Zod (defensive check)
|
|
159
|
-
const configValueSchema = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(configValueSchema), z.record(configValueSchema)]));
|
|
160
|
-
const configSchemaValidator = z.record(configValueSchema);
|
|
161
|
-
configSchemaValidator.parse(plugin.configSchema);
|
|
162
|
-
const configEntries = [];
|
|
163
|
-
for (const [key, value] of Object.entries(plugin.configSchema)) {
|
|
164
|
-
const formattedValue = this.formatValue(value, 1);
|
|
165
|
-
configEntries.push(` // ${plugin.name} plugin configuration\n ${key}: ${formattedValue}`);
|
|
166
|
-
}
|
|
167
|
-
return configEntries.join(',\n\n');
|
|
168
|
-
} catch (error) {
|
|
169
|
-
if (error instanceof z.ZodError) {
|
|
170
|
-
const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
|
|
171
|
-
output.warn(`Invalid config schema for plugin ${plugin.name}: ${messages.join(', ')}`);
|
|
172
|
-
} else {
|
|
173
|
-
output.warn(`Failed to format config for plugin ${plugin.name}: ${error.message}`);
|
|
174
|
-
}
|
|
175
|
-
return '';
|
|
176
|
-
}
|
|
147
|
+
return `${coreConfig}\n};\n`;
|
|
148
|
+
}
|
|
149
|
+
function writeHumanCreatedOutput(output, plugins) {
|
|
150
|
+
output.header('init');
|
|
151
|
+
output.complete('Created vizzly.config.js');
|
|
152
|
+
let pluginsWithConfig = getPluginsWithConfig(plugins);
|
|
153
|
+
if (pluginsWithConfig.length > 0) {
|
|
154
|
+
output.hint(`Added config for ${pluginsWithConfig.length} plugin(s):`);
|
|
155
|
+
output.list(pluginsWithConfig.map(plugin => plugin.name), {
|
|
156
|
+
indent: 4
|
|
157
|
+
});
|
|
177
158
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const formatted = this.formatValue(item, depth + 1);
|
|
196
|
-
return `${nextIndent}${formatted}`;
|
|
197
|
-
});
|
|
198
|
-
return `[\n${items.join(',\n')}\n${indent}]`;
|
|
199
|
-
}
|
|
200
|
-
if (typeof value === 'object') {
|
|
201
|
-
const entries = Object.entries(value);
|
|
202
|
-
if (entries.length === 0) return '{}';
|
|
203
|
-
const props = entries.map(([k, v]) => {
|
|
204
|
-
const formatted = this.formatValue(v, depth + 1);
|
|
205
|
-
return `${nextIndent}${k}: ${formatted}`;
|
|
206
|
-
});
|
|
207
|
-
return `{\n${props.join(',\n')}\n${indent}}`;
|
|
208
|
-
}
|
|
209
|
-
return String(value);
|
|
159
|
+
}
|
|
160
|
+
function showNextSteps(output) {
|
|
161
|
+
output.blank();
|
|
162
|
+
output.labelValue('Next steps', '');
|
|
163
|
+
output.list(['Set your API token: export VIZZLY_TOKEN="your-api-key"', 'Run your tests with Vizzly: npx vizzly run "npm test"', 'Upload screenshots: npx vizzly upload ./screenshots']);
|
|
164
|
+
}
|
|
165
|
+
async function writeConfigFile({
|
|
166
|
+
configPath,
|
|
167
|
+
plugins,
|
|
168
|
+
options,
|
|
169
|
+
output,
|
|
170
|
+
writeFile
|
|
171
|
+
}) {
|
|
172
|
+
let coreConfig = createConfigContent(plugins, output);
|
|
173
|
+
await writeFile(configPath, coreConfig, 'utf8');
|
|
174
|
+
if (!options.json) {
|
|
175
|
+
writeHumanCreatedOutput(output, plugins);
|
|
210
176
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
177
|
+
}
|
|
178
|
+
async function loadInitPlugins(options, deps) {
|
|
179
|
+
if (options.plugins) {
|
|
180
|
+
return options.plugins;
|
|
215
181
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
182
|
+
try {
|
|
183
|
+
let config = await deps.loadConfig(options.config, {});
|
|
184
|
+
return await deps.loadPlugins(options.config, config, null);
|
|
185
|
+
} catch {
|
|
186
|
+
return [];
|
|
223
187
|
}
|
|
224
188
|
}
|
|
225
189
|
|
|
226
190
|
// Export factory function for CLI
|
|
227
191
|
export function createInitCommand(options) {
|
|
228
|
-
|
|
229
|
-
return () => command.run(options);
|
|
192
|
+
return () => init(options);
|
|
230
193
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
194
|
+
export async function init(options = {}, deps = {}) {
|
|
195
|
+
let resolvedDeps = createInitDeps(deps);
|
|
196
|
+
configureOutput(resolvedDeps.output, options);
|
|
197
|
+
let plugins = await loadInitPlugins(options, resolvedDeps);
|
|
198
|
+
let configPath = getInitConfigPath(resolvedDeps.cwd());
|
|
199
|
+
let hasConfig = await fileExists(configPath, resolvedDeps.access);
|
|
200
|
+
if (hasConfig && !options.force) {
|
|
201
|
+
if (options.json) {
|
|
202
|
+
resolvedDeps.output.data({
|
|
203
|
+
status: 'skipped',
|
|
204
|
+
reason: 'config_exists',
|
|
205
|
+
configPath,
|
|
206
|
+
message: 'A vizzly.config.js file already exists. Use --force to overwrite.'
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
status: 'skipped',
|
|
210
|
+
configPath
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
resolvedDeps.output.header('init');
|
|
214
|
+
resolvedDeps.output.warn('A vizzly.config.js file already exists');
|
|
215
|
+
resolvedDeps.output.hint('Use --force to overwrite');
|
|
216
|
+
return {
|
|
217
|
+
status: 'skipped',
|
|
218
|
+
configPath
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
await writeConfigFile({
|
|
223
|
+
configPath,
|
|
224
|
+
plugins,
|
|
225
|
+
options,
|
|
226
|
+
output: resolvedDeps.output,
|
|
227
|
+
writeFile: resolvedDeps.writeFile
|
|
228
|
+
});
|
|
229
|
+
let pluginNames = getPluginConfigNames(plugins);
|
|
230
|
+
if (options.json) {
|
|
231
|
+
resolvedDeps.output.data({
|
|
232
|
+
status: 'created',
|
|
233
|
+
configPath,
|
|
234
|
+
plugins: pluginNames
|
|
235
|
+
});
|
|
236
|
+
return {
|
|
237
|
+
status: 'created',
|
|
238
|
+
configPath,
|
|
239
|
+
plugins: pluginNames
|
|
240
|
+
};
|
|
248
241
|
}
|
|
249
|
-
|
|
250
|
-
|
|
242
|
+
showNextSteps(resolvedDeps.output);
|
|
243
|
+
resolvedDeps.output.blank();
|
|
244
|
+
resolvedDeps.output.complete('Vizzly CLI setup complete');
|
|
245
|
+
return {
|
|
246
|
+
status: 'created',
|
|
247
|
+
configPath,
|
|
248
|
+
plugins: pluginNames
|
|
249
|
+
};
|
|
250
|
+
} catch (error) {
|
|
251
|
+
throw new VizzlyError('Failed to initialize Vizzly configuration', 'INIT_FAILED', {
|
|
252
|
+
error: error.message
|
|
253
|
+
});
|
|
251
254
|
}
|
|
252
|
-
const command = new InitCommand(plugins);
|
|
253
|
-
return await command.run(options);
|
|
254
255
|
}
|