@vizzly-testing/cli 0.20.0 → 0.20.1-beta.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/dist/api/client.js +134 -0
- package/dist/api/core.js +341 -0
- package/dist/api/endpoints.js +314 -0
- package/dist/api/index.js +19 -0
- package/dist/auth/client.js +91 -0
- package/dist/auth/core.js +176 -0
- package/dist/auth/index.js +30 -0
- package/dist/auth/operations.js +148 -0
- package/dist/cli.js +178 -3
- package/dist/client/index.js +144 -77
- package/dist/commands/doctor.js +121 -36
- package/dist/commands/finalize.js +49 -18
- package/dist/commands/init.js +13 -18
- package/dist/commands/login.js +49 -55
- package/dist/commands/logout.js +17 -9
- package/dist/commands/project.js +100 -71
- package/dist/commands/run.js +189 -95
- package/dist/commands/status.js +101 -66
- package/dist/commands/tdd-daemon.js +61 -32
- package/dist/commands/tdd.js +104 -98
- package/dist/commands/upload.js +78 -34
- package/dist/commands/whoami.js +44 -42
- package/dist/config/core.js +438 -0
- package/dist/config/index.js +13 -0
- package/dist/config/operations.js +327 -0
- package/dist/index.js +1 -1
- package/dist/project/core.js +295 -0
- package/dist/project/index.js +13 -0
- package/dist/project/operations.js +393 -0
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +16 -16
- package/dist/screenshot-server/core.js +157 -0
- package/dist/screenshot-server/index.js +11 -0
- package/dist/screenshot-server/operations.js +183 -0
- package/dist/sdk/index.js +3 -2
- package/dist/server/handlers/api-handler.js +14 -5
- package/dist/server/handlers/tdd-handler.js +191 -53
- 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 +186 -0
- package/dist/server-manager/index.js +81 -0
- package/dist/server-manager/operations.js +209 -0
- package/dist/services/build-manager.js +2 -69
- package/dist/services/index.js +21 -48
- package/dist/services/screenshot-server.js +40 -74
- package/dist/services/server-manager.js +45 -80
- package/dist/services/test-runner.js +90 -250
- package/dist/services/uploader.js +56 -358
- package/dist/tdd/core/hotspot-coverage.js +112 -0
- package/dist/tdd/core/signature.js +101 -0
- package/dist/tdd/index.js +19 -0
- package/dist/tdd/metadata/baseline-metadata.js +103 -0
- package/dist/tdd/metadata/hotspot-metadata.js +93 -0
- package/dist/tdd/services/baseline-downloader.js +151 -0
- package/dist/tdd/services/baseline-manager.js +166 -0
- package/dist/tdd/services/comparison-service.js +230 -0
- package/dist/tdd/services/hotspot-service.js +71 -0
- package/dist/tdd/services/result-service.js +123 -0
- package/dist/tdd/tdd-service.js +1145 -0
- package/dist/test-runner/core.js +255 -0
- package/dist/test-runner/index.js +13 -0
- package/dist/test-runner/operations.js +483 -0
- package/dist/types/client.d.ts +25 -2
- package/dist/uploader/core.js +396 -0
- package/dist/uploader/index.js +11 -0
- package/dist/uploader/operations.js +412 -0
- 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 -13
- package/dist/services/api-service.js +0 -412
- package/dist/services/auth-service.js +0 -226
- package/dist/services/config-service.js +0 -369
- package/dist/services/html-report-generator.js +0 -455
- package/dist/services/project-service.js +0 -326
- package/dist/services/report-generator/report.css +0 -411
- package/dist/services/report-generator/viewer.js +0 -102
- package/dist/services/static-report-generator.js +0 -207
- package/dist/services/tdd-service.js +0 -1437
package/dist/commands/doctor.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { URL } from 'node:url';
|
|
2
|
+
import { createApiClient, getBuilds } from '../api/index.js';
|
|
2
3
|
import { ConfigError } from '../errors/vizzly-error.js';
|
|
3
|
-
import { ApiService } from '../services/api-service.js';
|
|
4
4
|
import { loadConfig } from '../utils/config-loader.js';
|
|
5
|
+
import { getContext } from '../utils/context.js';
|
|
5
6
|
import { getApiToken } from '../utils/environment-config.js';
|
|
6
7
|
import * as output from '../utils/output.js';
|
|
7
8
|
|
|
@@ -16,7 +17,7 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
|
|
|
16
17
|
verbose: globalOptions.verbose,
|
|
17
18
|
color: !globalOptions.noColor
|
|
18
19
|
});
|
|
19
|
-
|
|
20
|
+
let diagnostics = {
|
|
20
21
|
environment: {
|
|
21
22
|
nodeVersion: null,
|
|
22
23
|
nodeVersionValid: null
|
|
@@ -35,107 +36,191 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
|
|
|
35
36
|
}
|
|
36
37
|
};
|
|
37
38
|
let hasErrors = false;
|
|
39
|
+
let checks = [];
|
|
38
40
|
try {
|
|
39
41
|
// Determine if we'll attempt remote checks (API connectivity)
|
|
40
|
-
|
|
42
|
+
let willCheckConnectivity = Boolean(options.api || getApiToken());
|
|
41
43
|
|
|
42
|
-
//
|
|
43
|
-
output.
|
|
44
|
+
// Show header
|
|
45
|
+
output.header('doctor', willCheckConnectivity ? 'full' : 'local');
|
|
44
46
|
|
|
45
47
|
// Node.js version check (require >= 20)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
let nodeVersion = process.version;
|
|
49
|
+
let nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
|
|
48
50
|
diagnostics.environment.nodeVersion = nodeVersion;
|
|
49
51
|
diagnostics.environment.nodeVersionValid = nodeMajor >= 20;
|
|
50
52
|
if (nodeMajor >= 20) {
|
|
51
|
-
|
|
53
|
+
checks.push({
|
|
54
|
+
name: 'Node.js',
|
|
55
|
+
value: `${nodeVersion} (supported)`,
|
|
56
|
+
ok: true
|
|
57
|
+
});
|
|
52
58
|
} else {
|
|
59
|
+
checks.push({
|
|
60
|
+
name: 'Node.js',
|
|
61
|
+
value: `${nodeVersion} (requires >= 20)`,
|
|
62
|
+
ok: false
|
|
63
|
+
});
|
|
53
64
|
hasErrors = true;
|
|
54
|
-
output.error('Node.js version must be >= 20');
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
// Load configuration (apply global CLI overrides like --config only)
|
|
58
|
-
|
|
68
|
+
let config = await loadConfig(globalOptions.config);
|
|
59
69
|
|
|
60
70
|
// Validate apiUrl
|
|
61
71
|
diagnostics.configuration.apiUrl = config.apiUrl;
|
|
62
72
|
try {
|
|
63
|
-
|
|
73
|
+
let url = new URL(config.apiUrl);
|
|
64
74
|
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
65
75
|
throw new ConfigError('URL must use http or https');
|
|
66
76
|
}
|
|
67
77
|
diagnostics.configuration.apiUrlValid = true;
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
checks.push({
|
|
79
|
+
name: 'API URL',
|
|
80
|
+
value: config.apiUrl,
|
|
81
|
+
ok: true
|
|
82
|
+
});
|
|
83
|
+
} catch (_e) {
|
|
70
84
|
diagnostics.configuration.apiUrlValid = false;
|
|
85
|
+
checks.push({
|
|
86
|
+
name: 'API URL',
|
|
87
|
+
value: 'invalid (check VIZZLY_API_URL)',
|
|
88
|
+
ok: false
|
|
89
|
+
});
|
|
71
90
|
hasErrors = true;
|
|
72
|
-
output.error('Invalid apiUrl in configuration (set VIZZLY_API_URL or config file)', e);
|
|
73
91
|
}
|
|
74
92
|
|
|
75
93
|
// Validate threshold (0..1 inclusive)
|
|
76
|
-
|
|
94
|
+
let threshold = Number(config?.comparison?.threshold);
|
|
77
95
|
diagnostics.configuration.threshold = threshold;
|
|
78
96
|
// CIEDE2000 threshold: 0 = exact, 1 = JND, 2 = recommended, 3+ = permissive
|
|
79
|
-
|
|
97
|
+
let thresholdValid = Number.isFinite(threshold) && threshold >= 0;
|
|
80
98
|
diagnostics.configuration.thresholdValid = thresholdValid;
|
|
81
99
|
if (thresholdValid) {
|
|
82
|
-
|
|
100
|
+
checks.push({
|
|
101
|
+
name: 'Threshold',
|
|
102
|
+
value: `${threshold} (CIEDE2000)`,
|
|
103
|
+
ok: true
|
|
104
|
+
});
|
|
83
105
|
} else {
|
|
106
|
+
checks.push({
|
|
107
|
+
name: 'Threshold',
|
|
108
|
+
value: 'invalid',
|
|
109
|
+
ok: false
|
|
110
|
+
});
|
|
84
111
|
hasErrors = true;
|
|
85
|
-
output.error('Invalid threshold (expected non-negative number)');
|
|
86
112
|
}
|
|
87
113
|
|
|
88
114
|
// Report effective port without binding
|
|
89
|
-
|
|
115
|
+
let port = config?.server?.port ?? 47392;
|
|
90
116
|
diagnostics.configuration.port = port;
|
|
91
|
-
|
|
117
|
+
checks.push({
|
|
118
|
+
name: 'Port',
|
|
119
|
+
value: String(port),
|
|
120
|
+
ok: true
|
|
121
|
+
});
|
|
92
122
|
|
|
93
123
|
// Optional: API connectivity check when --api is provided or VIZZLY_TOKEN is present
|
|
94
|
-
|
|
124
|
+
let autoApi = Boolean(getApiToken());
|
|
95
125
|
if (options.api || autoApi) {
|
|
96
126
|
diagnostics.connectivity.checked = true;
|
|
97
127
|
if (!config.apiKey) {
|
|
98
128
|
diagnostics.connectivity.ok = false;
|
|
99
129
|
diagnostics.connectivity.error = 'Missing API token (VIZZLY_TOKEN)';
|
|
130
|
+
checks.push({
|
|
131
|
+
name: 'API Token',
|
|
132
|
+
value: 'missing',
|
|
133
|
+
ok: false
|
|
134
|
+
});
|
|
100
135
|
hasErrors = true;
|
|
101
|
-
output.error('Missing API token for connectivity check');
|
|
102
136
|
} else {
|
|
103
|
-
output.
|
|
137
|
+
output.startSpinner('Checking API connectivity...');
|
|
104
138
|
try {
|
|
105
|
-
|
|
139
|
+
let client = createApiClient({
|
|
106
140
|
baseUrl: config.apiUrl,
|
|
107
141
|
token: config.apiKey,
|
|
108
142
|
command: 'doctor'
|
|
109
143
|
});
|
|
110
144
|
// Minimal, read-only call
|
|
111
|
-
await
|
|
145
|
+
await getBuilds(client, {
|
|
112
146
|
limit: 1
|
|
113
147
|
});
|
|
148
|
+
output.stopSpinner();
|
|
114
149
|
diagnostics.connectivity.ok = true;
|
|
115
|
-
|
|
150
|
+
checks.push({
|
|
151
|
+
name: 'API',
|
|
152
|
+
value: 'connected',
|
|
153
|
+
ok: true
|
|
154
|
+
});
|
|
116
155
|
} catch (err) {
|
|
156
|
+
output.stopSpinner();
|
|
117
157
|
diagnostics.connectivity.ok = false;
|
|
118
158
|
diagnostics.connectivity.error = err?.message || String(err);
|
|
159
|
+
checks.push({
|
|
160
|
+
name: 'API',
|
|
161
|
+
value: 'connection failed',
|
|
162
|
+
ok: false
|
|
163
|
+
});
|
|
119
164
|
hasErrors = true;
|
|
120
|
-
output.error('API connectivity failed', err);
|
|
121
165
|
}
|
|
122
166
|
}
|
|
123
167
|
}
|
|
124
168
|
|
|
125
|
-
//
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
} else {
|
|
129
|
-
output.success('Preflight passed.');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Emit structured data in json/verbose modes
|
|
133
|
-
if (globalOptions.json || globalOptions.verbose) {
|
|
169
|
+
// Output results
|
|
170
|
+
if (globalOptions.json) {
|
|
171
|
+
// JSON mode - structured output only
|
|
134
172
|
output.data({
|
|
135
173
|
passed: !hasErrors,
|
|
136
174
|
diagnostics,
|
|
137
175
|
timestamp: new Date().toISOString()
|
|
138
176
|
});
|
|
177
|
+
} else {
|
|
178
|
+
// Human-readable output - display results as a checklist
|
|
179
|
+
// Use printErr to match header (both on stderr for consistent ordering)
|
|
180
|
+
let colors = output.getColors();
|
|
181
|
+
for (let check of checks) {
|
|
182
|
+
let icon = check.ok ? colors.brand.success('✓') : colors.brand.danger('✗');
|
|
183
|
+
let label = colors.brand.textTertiary(check.name.padEnd(12));
|
|
184
|
+
output.printErr(` ${icon} ${label} ${check.value}`);
|
|
185
|
+
}
|
|
186
|
+
output.printErr('');
|
|
187
|
+
|
|
188
|
+
// Summary
|
|
189
|
+
if (hasErrors) {
|
|
190
|
+
output.warn('Preflight completed with issues');
|
|
191
|
+
} else {
|
|
192
|
+
output.printErr(` ${colors.brand.success('✓')} Preflight passed`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Dynamic context section (same as help output)
|
|
196
|
+
let contextItems = getContext();
|
|
197
|
+
if (contextItems.length > 0) {
|
|
198
|
+
output.printErr('');
|
|
199
|
+
output.printErr(` ${colors.dim('─'.repeat(52))}`);
|
|
200
|
+
for (let item of contextItems) {
|
|
201
|
+
if (item.type === 'success') {
|
|
202
|
+
output.printErr(` ${colors.green('✓')} ${colors.gray(item.label)} ${colors.white(item.value)}`);
|
|
203
|
+
} else if (item.type === 'warning') {
|
|
204
|
+
output.printErr(` ${colors.yellow('!')} ${colors.gray(item.label)} ${colors.yellow(item.value)}`);
|
|
205
|
+
} else {
|
|
206
|
+
output.printErr(` ${colors.dim('○')} ${colors.gray(item.label)} ${colors.dim(item.value)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Footer with links
|
|
212
|
+
output.printErr('');
|
|
213
|
+
output.printErr(` ${colors.dim('─'.repeat(52))}`);
|
|
214
|
+
output.printErr(` ${colors.dim('Docs')} ${colors.cyan(colors.underline('docs.vizzly.dev'))} ${colors.dim('GitHub')} ${colors.cyan(colors.underline('github.com/vizzly-testing/cli'))}`);
|
|
215
|
+
|
|
216
|
+
// Emit structured data in verbose mode (in addition to visual output)
|
|
217
|
+
if (globalOptions.verbose) {
|
|
218
|
+
output.data({
|
|
219
|
+
passed: !hasErrors,
|
|
220
|
+
diagnostics,
|
|
221
|
+
timestamp: new Date().toISOString()
|
|
222
|
+
});
|
|
223
|
+
}
|
|
139
224
|
}
|
|
140
225
|
} catch (error) {
|
|
141
226
|
hasErrors = true;
|
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Finalize command implementation
|
|
3
|
+
* Uses functional API operations directly
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createApiClient as defaultCreateApiClient, finalizeParallelBuild as defaultFinalizeParallelBuild } from '../api/index.js';
|
|
7
|
+
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
8
|
+
import * as defaultOutput from '../utils/output.js';
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* Finalize command implementation
|
|
7
12
|
* @param {string} parallelId - Parallel ID to finalize
|
|
8
13
|
* @param {Object} options - Command options
|
|
9
14
|
* @param {Object} globalOptions - Global CLI options
|
|
15
|
+
* @param {Object} deps - Dependencies for testing
|
|
10
16
|
*/
|
|
11
|
-
export async function finalizeCommand(parallelId, options = {}, globalOptions = {}) {
|
|
17
|
+
export async function finalizeCommand(parallelId, options = {}, globalOptions = {}, deps = {}) {
|
|
18
|
+
let {
|
|
19
|
+
loadConfig = defaultLoadConfig,
|
|
20
|
+
createApiClient = defaultCreateApiClient,
|
|
21
|
+
finalizeParallelBuild = defaultFinalizeParallelBuild,
|
|
22
|
+
output = defaultOutput,
|
|
23
|
+
exit = code => process.exit(code)
|
|
24
|
+
} = deps;
|
|
12
25
|
output.configure({
|
|
13
26
|
json: globalOptions.json,
|
|
14
27
|
verbose: globalOptions.verbose,
|
|
@@ -16,16 +29,20 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
|
|
|
16
29
|
});
|
|
17
30
|
try {
|
|
18
31
|
// Load configuration with CLI overrides
|
|
19
|
-
|
|
32
|
+
let allOptions = {
|
|
20
33
|
...globalOptions,
|
|
21
34
|
...options
|
|
22
35
|
};
|
|
23
|
-
|
|
36
|
+
let config = await loadConfig(globalOptions.config, allOptions);
|
|
24
37
|
|
|
25
38
|
// Validate API token
|
|
26
39
|
if (!config.apiKey) {
|
|
27
40
|
output.error('API token required. Use --token or set VIZZLY_TOKEN environment variable');
|
|
28
|
-
|
|
41
|
+
exit(1);
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
reason: 'no-api-key'
|
|
45
|
+
};
|
|
29
46
|
}
|
|
30
47
|
if (globalOptions.verbose) {
|
|
31
48
|
output.info('Configuration loaded');
|
|
@@ -35,25 +52,39 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
|
|
|
35
52
|
});
|
|
36
53
|
}
|
|
37
54
|
|
|
38
|
-
//
|
|
55
|
+
// Call finalize endpoint via functional API
|
|
39
56
|
output.startSpinner('Finalizing parallel build...');
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
let client = createApiClient({
|
|
58
|
+
baseUrl: config.apiUrl,
|
|
59
|
+
token: config.apiKey,
|
|
60
|
+
command: 'finalize'
|
|
61
|
+
});
|
|
62
|
+
let result = await finalizeParallelBuild(client, parallelId);
|
|
42
63
|
output.stopSpinner();
|
|
43
|
-
|
|
44
|
-
// Call finalize endpoint
|
|
45
|
-
const result = await apiService.finalizeParallelBuild(parallelId);
|
|
46
64
|
if (globalOptions.json) {
|
|
47
65
|
output.data(result);
|
|
48
66
|
} else {
|
|
49
|
-
output.
|
|
50
|
-
output.
|
|
51
|
-
output.
|
|
67
|
+
output.header('finalize');
|
|
68
|
+
output.complete(`Parallel build finalized`);
|
|
69
|
+
output.blank();
|
|
70
|
+
output.keyValue({
|
|
71
|
+
Build: result.build.id,
|
|
72
|
+
Status: result.build.status,
|
|
73
|
+
'Parallel ID': result.build.parallel_id
|
|
74
|
+
});
|
|
52
75
|
}
|
|
76
|
+
return {
|
|
77
|
+
success: true,
|
|
78
|
+
result
|
|
79
|
+
};
|
|
53
80
|
} catch (error) {
|
|
54
81
|
output.stopSpinner();
|
|
55
82
|
output.error('Failed to finalize parallel build', error);
|
|
56
|
-
|
|
83
|
+
exit(1);
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
error
|
|
87
|
+
};
|
|
57
88
|
} finally {
|
|
58
89
|
output.cleanup();
|
|
59
90
|
}
|
|
@@ -65,7 +96,7 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
|
|
|
65
96
|
* @param {Object} options - Command options
|
|
66
97
|
*/
|
|
67
98
|
export function validateFinalizeOptions(parallelId, _options) {
|
|
68
|
-
|
|
99
|
+
let errors = [];
|
|
69
100
|
if (!parallelId || parallelId.trim() === '') {
|
|
70
101
|
errors.push('Parallel ID is required');
|
|
71
102
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -15,14 +15,14 @@ export class InitCommand {
|
|
|
15
15
|
this.plugins = plugins;
|
|
16
16
|
}
|
|
17
17
|
async run(options = {}) {
|
|
18
|
-
output.
|
|
19
|
-
output.blank();
|
|
18
|
+
output.header('init');
|
|
20
19
|
try {
|
|
21
20
|
// Check for existing config
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
let configPath = path.join(process.cwd(), 'vizzly.config.js');
|
|
22
|
+
let hasConfig = await this.fileExists(configPath);
|
|
24
23
|
if (hasConfig && !options.force) {
|
|
25
|
-
output.
|
|
24
|
+
output.warn('A vizzly.config.js file already exists');
|
|
25
|
+
output.hint('Use --force to overwrite');
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -32,7 +32,7 @@ export class InitCommand {
|
|
|
32
32
|
// Show next steps
|
|
33
33
|
this.showNextSteps();
|
|
34
34
|
output.blank();
|
|
35
|
-
output.
|
|
35
|
+
output.complete('Vizzly CLI setup complete');
|
|
36
36
|
} catch (error) {
|
|
37
37
|
throw new VizzlyError('Failed to initialize Vizzly configuration', 'INIT_FAILED', {
|
|
38
38
|
error: error.message
|
|
@@ -77,14 +77,14 @@ export class InitCommand {
|
|
|
77
77
|
}
|
|
78
78
|
coreConfig += '\n};\n';
|
|
79
79
|
await fs.writeFile(configPath, coreConfig, 'utf8');
|
|
80
|
-
output.
|
|
80
|
+
output.complete('Created vizzly.config.js');
|
|
81
81
|
|
|
82
82
|
// Log discovered plugins
|
|
83
|
-
|
|
83
|
+
let pluginsWithConfig = this.plugins.filter(p => p.configSchema);
|
|
84
84
|
if (pluginsWithConfig.length > 0) {
|
|
85
|
-
output.
|
|
86
|
-
pluginsWithConfig.
|
|
87
|
-
|
|
85
|
+
output.hint(`Added config for ${pluginsWithConfig.length} plugin(s):`);
|
|
86
|
+
output.list(pluginsWithConfig.map(p => p.name), {
|
|
87
|
+
indent: 4
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -168,13 +168,8 @@ export class InitCommand {
|
|
|
168
168
|
}
|
|
169
169
|
showNextSteps() {
|
|
170
170
|
output.blank();
|
|
171
|
-
output.
|
|
172
|
-
output.
|
|
173
|
-
output.info(' export VIZZLY_TOKEN="your-api-key"');
|
|
174
|
-
output.info(' 2. Run your tests with Vizzly:');
|
|
175
|
-
output.info(' npx vizzly run "npm test"');
|
|
176
|
-
output.info(' 3. Upload screenshots:');
|
|
177
|
-
output.info(' npx vizzly upload ./screenshots');
|
|
171
|
+
output.labelValue('Next steps', '');
|
|
172
|
+
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']);
|
|
178
173
|
}
|
|
179
174
|
async fileExists(filePath) {
|
|
180
175
|
try {
|
package/dist/commands/login.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Authenticates user via OAuth device flow
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { completeDeviceFlow, createAuthClient, createTokenStore, initiateDeviceFlow, pollDeviceAuthorization } from '../auth/index.js';
|
|
7
7
|
import { openBrowser } from '../utils/browser.js';
|
|
8
8
|
import { getApiUrl } from '../utils/environment-config.js';
|
|
9
9
|
import * as output from '../utils/output.js';
|
|
@@ -19,56 +19,49 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
19
19
|
verbose: globalOptions.verbose,
|
|
20
20
|
color: !globalOptions.noColor
|
|
21
21
|
});
|
|
22
|
-
|
|
22
|
+
let colors = output.getColors();
|
|
23
23
|
try {
|
|
24
|
-
output.
|
|
25
|
-
output.blank();
|
|
24
|
+
output.header('login');
|
|
26
25
|
|
|
27
|
-
// Create auth
|
|
28
|
-
|
|
26
|
+
// Create auth client and token store
|
|
27
|
+
let client = createAuthClient({
|
|
29
28
|
baseUrl: options.apiUrl || getApiUrl()
|
|
30
29
|
});
|
|
30
|
+
let tokenStore = createTokenStore();
|
|
31
31
|
|
|
32
32
|
// Initiate device flow
|
|
33
33
|
output.startSpinner('Connecting to Vizzly...');
|
|
34
|
-
|
|
34
|
+
let deviceFlow = await initiateDeviceFlow(client);
|
|
35
35
|
output.stopSpinner();
|
|
36
36
|
|
|
37
37
|
// Handle both snake_case and camelCase field names
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
let verificationUri = deviceFlow.verification_uri || deviceFlow.verificationUri;
|
|
39
|
+
let userCode = deviceFlow.user_code || deviceFlow.userCode;
|
|
40
|
+
let deviceCode = deviceFlow.device_code || deviceFlow.deviceCode;
|
|
41
41
|
if (!verificationUri || !userCode || !deviceCode) {
|
|
42
42
|
throw new Error('Invalid device flow response from server');
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Build URL with pre-filled code
|
|
46
|
-
|
|
46
|
+
let urlWithCode = `${verificationUri}?code=${userCode}`;
|
|
47
47
|
|
|
48
|
-
// Display user code prominently
|
|
49
|
-
output.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
output.blank();
|
|
54
|
-
output.print(` ${urlWithCode}`);
|
|
55
|
-
output.blank();
|
|
56
|
-
output.print(' Your code (pre-filled):');
|
|
57
|
-
output.blank();
|
|
58
|
-
output.print(` ${colors.bold(colors.cyan(userCode))}`);
|
|
59
|
-
output.blank();
|
|
60
|
-
output.print('='.repeat(50));
|
|
48
|
+
// Display user code prominently in a box
|
|
49
|
+
output.printBox(['Visit this URL to authorize:', '', colors.brand.info(urlWithCode), '', 'Your code:', '', colors.bold(colors.brand.amber(userCode))], {
|
|
50
|
+
title: 'Authorization',
|
|
51
|
+
style: 'branded'
|
|
52
|
+
});
|
|
61
53
|
output.blank();
|
|
62
54
|
|
|
63
55
|
// Try to open browser with pre-filled code
|
|
64
|
-
|
|
56
|
+
let browserOpened = await openBrowser(urlWithCode);
|
|
65
57
|
if (browserOpened) {
|
|
66
|
-
output.
|
|
58
|
+
output.complete('Browser opened');
|
|
67
59
|
} else {
|
|
68
|
-
output.warn('Could not open browser automatically
|
|
60
|
+
output.warn('Could not open browser automatically');
|
|
61
|
+
output.hint('Please open the URL manually');
|
|
69
62
|
}
|
|
70
63
|
output.blank();
|
|
71
|
-
output.
|
|
64
|
+
output.hint('After authorizing, press Enter to continue...');
|
|
72
65
|
|
|
73
66
|
// Wait for user to press Enter
|
|
74
67
|
await new Promise(resolve => {
|
|
@@ -82,8 +75,8 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
82
75
|
});
|
|
83
76
|
|
|
84
77
|
// Check authorization status
|
|
85
|
-
output.startSpinner('Checking authorization
|
|
86
|
-
|
|
78
|
+
output.startSpinner('Checking authorization...');
|
|
79
|
+
let pollResponse = await pollDeviceAuthorization(client, deviceCode);
|
|
87
80
|
output.stopSpinner();
|
|
88
81
|
let tokenData = null;
|
|
89
82
|
|
|
@@ -103,55 +96,56 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
103
96
|
|
|
104
97
|
// Complete device flow and save tokens
|
|
105
98
|
// Handle both snake_case and camelCase for token data, and nested tokens object
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
let tokensData = tokenData.tokens || tokenData;
|
|
100
|
+
let tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
|
|
101
|
+
let tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : tokenData.expires_at || tokenData.expiresAt;
|
|
102
|
+
let tokens = {
|
|
110
103
|
accessToken: tokensData.accessToken || tokensData.access_token,
|
|
111
104
|
refreshToken: tokensData.refreshToken || tokensData.refresh_token,
|
|
112
105
|
expiresAt: tokenExpiresAt,
|
|
113
106
|
user: tokenData.user,
|
|
114
107
|
organizations: tokenData.organizations
|
|
115
108
|
};
|
|
116
|
-
await
|
|
109
|
+
await completeDeviceFlow(tokenStore, tokens);
|
|
117
110
|
|
|
118
|
-
// Display success
|
|
119
|
-
output.
|
|
111
|
+
// Display success
|
|
112
|
+
output.complete('Authenticated');
|
|
120
113
|
output.blank();
|
|
121
114
|
|
|
122
115
|
// Show user info
|
|
123
116
|
if (tokens.user) {
|
|
124
|
-
output.
|
|
125
|
-
|
|
117
|
+
output.keyValue({
|
|
118
|
+
User: tokens.user.name || tokens.user.username,
|
|
119
|
+
Email: tokens.user.email
|
|
120
|
+
});
|
|
126
121
|
}
|
|
127
122
|
|
|
128
123
|
// Show organization info
|
|
129
124
|
if (tokens.organizations && tokens.organizations.length > 0) {
|
|
130
125
|
output.blank();
|
|
131
|
-
output.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
126
|
+
output.labelValue('Organizations', '');
|
|
127
|
+
let orgItems = tokens.organizations.map(org => `${org.name}${org.slug ? ` (@${org.slug})` : ''}`);
|
|
128
|
+
output.list(orgItems);
|
|
135
129
|
}
|
|
136
130
|
|
|
137
131
|
// Show token expiry info
|
|
138
132
|
if (tokens.expiresAt) {
|
|
139
133
|
output.blank();
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
134
|
+
let expiresAt = new Date(tokens.expiresAt);
|
|
135
|
+
let msUntilExpiry = expiresAt.getTime() - Date.now();
|
|
136
|
+
let daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
|
|
137
|
+
let hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
|
|
138
|
+
let minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
|
|
145
139
|
if (daysUntilExpiry > 0) {
|
|
146
|
-
output.
|
|
140
|
+
output.hint(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''}`);
|
|
147
141
|
} else if (hoursUntilExpiry > 0) {
|
|
148
|
-
output.
|
|
142
|
+
output.hint(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`);
|
|
149
143
|
} else if (minutesUntilExpiry > 0) {
|
|
150
|
-
output.
|
|
144
|
+
output.hint(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
|
|
151
145
|
}
|
|
152
146
|
}
|
|
153
147
|
output.blank();
|
|
154
|
-
output.
|
|
148
|
+
output.hint('You can now use Vizzly CLI commands without VIZZLY_TOKEN');
|
|
155
149
|
output.cleanup();
|
|
156
150
|
} catch (error) {
|
|
157
151
|
output.stopSpinner();
|
|
@@ -160,13 +154,13 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
160
154
|
if (error.name === 'AuthError') {
|
|
161
155
|
output.error('Authentication failed', error);
|
|
162
156
|
output.blank();
|
|
163
|
-
output.
|
|
164
|
-
output.
|
|
157
|
+
output.hint('Please try logging in again');
|
|
158
|
+
output.hint("If you don't have an account, sign up at https://vizzly.dev");
|
|
165
159
|
process.exit(1);
|
|
166
160
|
} else if (error.code === 'RATE_LIMIT_ERROR') {
|
|
167
161
|
output.error('Too many login attempts', error);
|
|
168
162
|
output.blank();
|
|
169
|
-
output.
|
|
163
|
+
output.hint('Please wait a few minutes before trying again');
|
|
170
164
|
process.exit(1);
|
|
171
165
|
} else {
|
|
172
166
|
output.error('Login failed', error);
|
package/dist/commands/logout.js
CHANGED
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
* Clears stored authentication tokens
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { createAuthClient, createTokenStore, getAuthTokens, logout } from '../auth/index.js';
|
|
7
7
|
import { getApiUrl } from '../utils/environment-config.js';
|
|
8
|
-
import { getAuthTokens } from '../utils/global-config.js';
|
|
9
8
|
import * as output from '../utils/output.js';
|
|
10
9
|
|
|
11
10
|
/**
|
|
@@ -21,29 +20,38 @@ export async function logoutCommand(options = {}, globalOptions = {}) {
|
|
|
21
20
|
});
|
|
22
21
|
try {
|
|
23
22
|
// Check if user is logged in
|
|
24
|
-
|
|
23
|
+
let auth = await getAuthTokens();
|
|
25
24
|
if (!auth || !auth.accessToken) {
|
|
26
|
-
|
|
25
|
+
if (globalOptions.json) {
|
|
26
|
+
output.data({
|
|
27
|
+
loggedOut: false,
|
|
28
|
+
reason: 'not_logged_in'
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
output.header('logout');
|
|
32
|
+
output.print(' Not logged in');
|
|
33
|
+
}
|
|
27
34
|
output.cleanup();
|
|
28
35
|
return;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
// Logout
|
|
32
39
|
output.startSpinner('Logging out...');
|
|
33
|
-
|
|
40
|
+
let client = createAuthClient({
|
|
34
41
|
baseUrl: options.apiUrl || getApiUrl()
|
|
35
42
|
});
|
|
36
|
-
|
|
43
|
+
let tokenStore = createTokenStore();
|
|
44
|
+
await logout(client, tokenStore);
|
|
37
45
|
output.stopSpinner();
|
|
38
|
-
output.success('Successfully logged out');
|
|
39
46
|
if (globalOptions.json) {
|
|
40
47
|
output.data({
|
|
41
48
|
loggedOut: true
|
|
42
49
|
});
|
|
43
50
|
} else {
|
|
51
|
+
output.header('logout');
|
|
52
|
+
output.complete('Logged out');
|
|
44
53
|
output.blank();
|
|
45
|
-
output.
|
|
46
|
-
output.info('Run "vizzly login" to authenticate again');
|
|
54
|
+
output.hint('Run "vizzly login" to authenticate again');
|
|
47
55
|
}
|
|
48
56
|
output.cleanup();
|
|
49
57
|
} catch (error) {
|