@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
package/dist/commands/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@ import { URL } from 'node:url';
|
|
|
2
2
|
import { createApiClient, getBuilds } from '../api/index.js';
|
|
3
3
|
import { ConfigError } from '../errors/vizzly-error.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,72 +36,105 @@ 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,
|
|
@@ -111,31 +145,82 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
|
|
|
111
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;
|
|
@@ -64,9 +64,14 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
|
|
|
64
64
|
if (globalOptions.json) {
|
|
65
65
|
output.data(result);
|
|
66
66
|
} else {
|
|
67
|
-
output.
|
|
68
|
-
output.
|
|
69
|
-
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
|
+
});
|
|
70
75
|
}
|
|
71
76
|
return {
|
|
72
77
|
success: true,
|
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
|
@@ -19,10 +19,9 @@ 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
26
|
// Create auth client and token store
|
|
28
27
|
let client = createAuthClient({
|
|
@@ -36,40 +35,33 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
36
35
|
output.stopSpinner();
|
|
37
36
|
|
|
38
37
|
// Handle both snake_case and camelCase field names
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
let verificationUri = deviceFlow.verification_uri || deviceFlow.verificationUri;
|
|
39
|
+
let userCode = deviceFlow.user_code || deviceFlow.userCode;
|
|
40
|
+
let deviceCode = deviceFlow.device_code || deviceFlow.deviceCode;
|
|
42
41
|
if (!verificationUri || !userCode || !deviceCode) {
|
|
43
42
|
throw new Error('Invalid device flow response from server');
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
// Build URL with pre-filled code
|
|
47
|
-
|
|
46
|
+
let urlWithCode = `${verificationUri}?code=${userCode}`;
|
|
48
47
|
|
|
49
|
-
// Display user code prominently
|
|
50
|
-
output.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
output.blank();
|
|
55
|
-
output.print(` ${urlWithCode}`);
|
|
56
|
-
output.blank();
|
|
57
|
-
output.print(' Your code (pre-filled):');
|
|
58
|
-
output.blank();
|
|
59
|
-
output.print(` ${colors.bold(colors.cyan(userCode))}`);
|
|
60
|
-
output.blank();
|
|
61
|
-
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
|
+
});
|
|
62
53
|
output.blank();
|
|
63
54
|
|
|
64
55
|
// Try to open browser with pre-filled code
|
|
65
|
-
|
|
56
|
+
let browserOpened = await openBrowser(urlWithCode);
|
|
66
57
|
if (browserOpened) {
|
|
67
|
-
output.
|
|
58
|
+
output.complete('Browser opened');
|
|
68
59
|
} else {
|
|
69
|
-
output.warn('Could not open browser automatically
|
|
60
|
+
output.warn('Could not open browser automatically');
|
|
61
|
+
output.hint('Please open the URL manually');
|
|
70
62
|
}
|
|
71
63
|
output.blank();
|
|
72
|
-
output.
|
|
64
|
+
output.hint('After authorizing, press Enter to continue...');
|
|
73
65
|
|
|
74
66
|
// Wait for user to press Enter
|
|
75
67
|
await new Promise(resolve => {
|
|
@@ -83,7 +75,7 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
83
75
|
});
|
|
84
76
|
|
|
85
77
|
// Check authorization status
|
|
86
|
-
output.startSpinner('Checking authorization
|
|
78
|
+
output.startSpinner('Checking authorization...');
|
|
87
79
|
let pollResponse = await pollDeviceAuthorization(client, deviceCode);
|
|
88
80
|
output.stopSpinner();
|
|
89
81
|
let tokenData = null;
|
|
@@ -104,10 +96,10 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
104
96
|
|
|
105
97
|
// Complete device flow and save tokens
|
|
106
98
|
// Handle both snake_case and camelCase for token data, and nested tokens object
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 = {
|
|
111
103
|
accessToken: tokensData.accessToken || tokensData.access_token,
|
|
112
104
|
refreshToken: tokensData.refreshToken || tokensData.refresh_token,
|
|
113
105
|
expiresAt: tokenExpiresAt,
|
|
@@ -116,43 +108,44 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
116
108
|
};
|
|
117
109
|
await completeDeviceFlow(tokenStore, tokens);
|
|
118
110
|
|
|
119
|
-
// Display success
|
|
120
|
-
output.
|
|
111
|
+
// Display success
|
|
112
|
+
output.complete('Authenticated');
|
|
121
113
|
output.blank();
|
|
122
114
|
|
|
123
115
|
// Show user info
|
|
124
116
|
if (tokens.user) {
|
|
125
|
-
output.
|
|
126
|
-
|
|
117
|
+
output.keyValue({
|
|
118
|
+
User: tokens.user.name || tokens.user.username,
|
|
119
|
+
Email: tokens.user.email
|
|
120
|
+
});
|
|
127
121
|
}
|
|
128
122
|
|
|
129
123
|
// Show organization info
|
|
130
124
|
if (tokens.organizations && tokens.organizations.length > 0) {
|
|
131
125
|
output.blank();
|
|
132
|
-
output.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
126
|
+
output.labelValue('Organizations', '');
|
|
127
|
+
let orgItems = tokens.organizations.map(org => `${org.name}${org.slug ? ` (@${org.slug})` : ''}`);
|
|
128
|
+
output.list(orgItems);
|
|
136
129
|
}
|
|
137
130
|
|
|
138
131
|
// Show token expiry info
|
|
139
132
|
if (tokens.expiresAt) {
|
|
140
133
|
output.blank();
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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));
|
|
146
139
|
if (daysUntilExpiry > 0) {
|
|
147
|
-
output.
|
|
140
|
+
output.hint(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''}`);
|
|
148
141
|
} else if (hoursUntilExpiry > 0) {
|
|
149
|
-
output.
|
|
142
|
+
output.hint(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`);
|
|
150
143
|
} else if (minutesUntilExpiry > 0) {
|
|
151
|
-
output.
|
|
144
|
+
output.hint(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
|
|
152
145
|
}
|
|
153
146
|
}
|
|
154
147
|
output.blank();
|
|
155
|
-
output.
|
|
148
|
+
output.hint('You can now use Vizzly CLI commands without VIZZLY_TOKEN');
|
|
156
149
|
output.cleanup();
|
|
157
150
|
} catch (error) {
|
|
158
151
|
output.stopSpinner();
|
|
@@ -161,13 +154,13 @@ export async function loginCommand(options = {}, globalOptions = {}) {
|
|
|
161
154
|
if (error.name === 'AuthError') {
|
|
162
155
|
output.error('Authentication failed', error);
|
|
163
156
|
output.blank();
|
|
164
|
-
output.
|
|
165
|
-
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");
|
|
166
159
|
process.exit(1);
|
|
167
160
|
} else if (error.code === 'RATE_LIMIT_ERROR') {
|
|
168
161
|
output.error('Too many login attempts', error);
|
|
169
162
|
output.blank();
|
|
170
|
-
output.
|
|
163
|
+
output.hint('Please wait a few minutes before trying again');
|
|
171
164
|
process.exit(1);
|
|
172
165
|
} else {
|
|
173
166
|
output.error('Login failed', error);
|
package/dist/commands/logout.js
CHANGED
|
@@ -20,9 +20,17 @@ export async function logoutCommand(options = {}, globalOptions = {}) {
|
|
|
20
20
|
});
|
|
21
21
|
try {
|
|
22
22
|
// Check if user is logged in
|
|
23
|
-
|
|
23
|
+
let auth = await getAuthTokens();
|
|
24
24
|
if (!auth || !auth.accessToken) {
|
|
25
|
-
|
|
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
|
+
}
|
|
26
34
|
output.cleanup();
|
|
27
35
|
return;
|
|
28
36
|
}
|
|
@@ -35,15 +43,15 @@ export async function logoutCommand(options = {}, globalOptions = {}) {
|
|
|
35
43
|
let tokenStore = createTokenStore();
|
|
36
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) {
|