@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.
Files changed (38) hide show
  1. package/README.md +16 -18
  2. package/dist/cli.js +177 -2
  3. package/dist/client/index.js +144 -77
  4. package/dist/commands/doctor.js +118 -33
  5. package/dist/commands/finalize.js +8 -3
  6. package/dist/commands/init.js +13 -18
  7. package/dist/commands/login.js +42 -49
  8. package/dist/commands/logout.js +13 -5
  9. package/dist/commands/project.js +95 -67
  10. package/dist/commands/run.js +32 -6
  11. package/dist/commands/status.js +81 -50
  12. package/dist/commands/tdd-daemon.js +61 -32
  13. package/dist/commands/tdd.js +14 -26
  14. package/dist/commands/upload.js +18 -9
  15. package/dist/commands/whoami.js +40 -38
  16. package/dist/reporter/reporter-bundle.css +1 -1
  17. package/dist/reporter/reporter-bundle.iife.js +204 -22
  18. package/dist/server/handlers/tdd-handler.js +113 -7
  19. package/dist/server/http-server.js +9 -3
  20. package/dist/server/routers/baseline.js +58 -0
  21. package/dist/server/routers/dashboard.js +10 -6
  22. package/dist/server/routers/screenshot.js +32 -0
  23. package/dist/server-manager/core.js +5 -2
  24. package/dist/server-manager/operations.js +2 -1
  25. package/dist/services/config-service.js +306 -0
  26. package/dist/tdd/tdd-service.js +190 -126
  27. package/dist/types/client.d.ts +25 -2
  28. package/dist/utils/colors.js +187 -39
  29. package/dist/utils/config-loader.js +3 -6
  30. package/dist/utils/context.js +228 -0
  31. package/dist/utils/output.js +449 -14
  32. package/docs/api-reference.md +173 -8
  33. package/docs/tui-elements.md +560 -0
  34. package/package.json +13 -7
  35. package/dist/report-generator/core.js +0 -315
  36. package/dist/report-generator/index.js +0 -8
  37. package/dist/report-generator/operations.js +0 -196
  38. package/dist/services/static-report-generator.js +0 -65
@@ -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
- const diagnostics = {
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
- const willCheckConnectivity = Boolean(options.api || getApiToken());
42
+ let willCheckConnectivity = Boolean(options.api || getApiToken());
41
43
 
42
- // Announce preflight, indicating local-only when no token/connectivity is planned
43
- output.info(`Running Vizzly preflight${willCheckConnectivity ? '' : ' (local checks only)'}...`);
44
+ // Show header
45
+ output.header('doctor', willCheckConnectivity ? 'full' : 'local');
44
46
 
45
47
  // Node.js version check (require >= 20)
46
- const nodeVersion = process.version;
47
- const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
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
- output.success(`Node.js version: ${nodeVersion} (supported)`);
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
- const config = await loadConfig(globalOptions.config);
68
+ let config = await loadConfig(globalOptions.config);
59
69
 
60
70
  // Validate apiUrl
61
71
  diagnostics.configuration.apiUrl = config.apiUrl;
62
72
  try {
63
- const url = new URL(config.apiUrl);
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
- output.success(`API URL: ${config.apiUrl}`);
69
- } catch (e) {
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
- const threshold = Number(config?.comparison?.threshold);
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
- const thresholdValid = Number.isFinite(threshold) && threshold >= 0;
97
+ let thresholdValid = Number.isFinite(threshold) && threshold >= 0;
80
98
  diagnostics.configuration.thresholdValid = thresholdValid;
81
99
  if (thresholdValid) {
82
- output.success(`Threshold: ${threshold} (CIEDE2000 Delta E)`);
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
- const port = config?.server?.port ?? 47392;
115
+ let port = config?.server?.port ?? 47392;
90
116
  diagnostics.configuration.port = port;
91
- output.info(`Effective port: ${port}`);
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
- const autoApi = Boolean(getApiToken());
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.progress('Checking API connectivity...');
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
- output.success('API connectivity OK');
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
- // Summary
126
- if (hasErrors) {
127
- output.warn('Preflight completed with issues.');
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.success(`Parallel build ${result.build.id} finalized successfully`);
68
- output.info(`Status: ${result.build.status}`);
69
- output.info(`Parallel ID: ${result.build.parallel_id}`);
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,
@@ -15,14 +15,14 @@ export class InitCommand {
15
15
  this.plugins = plugins;
16
16
  }
17
17
  async run(options = {}) {
18
- output.info('🎯 Initializing Vizzly configuration...');
19
- output.blank();
18
+ output.header('init');
20
19
  try {
21
20
  // Check for existing config
22
- const configPath = path.join(process.cwd(), 'vizzly.config.js');
23
- const hasConfig = await this.fileExists(configPath);
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.info('A vizzly.config.js file already exists. Use --force to overwrite.');
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.success('Vizzly CLI setup complete!');
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.info(`📄 Created vizzly.config.js`);
80
+ output.complete('Created vizzly.config.js');
81
81
 
82
82
  // Log discovered plugins
83
- const pluginsWithConfig = this.plugins.filter(p => p.configSchema);
83
+ let pluginsWithConfig = this.plugins.filter(p => p.configSchema);
84
84
  if (pluginsWithConfig.length > 0) {
85
- output.info(` Added config for ${pluginsWithConfig.length} plugin(s):`);
86
- pluginsWithConfig.forEach(p => {
87
- output.info(` - ${p.name}`);
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.info('📚 Next steps:');
172
- output.info(' 1. Set your API token:');
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 {
@@ -19,10 +19,9 @@ export async function loginCommand(options = {}, globalOptions = {}) {
19
19
  verbose: globalOptions.verbose,
20
20
  color: !globalOptions.noColor
21
21
  });
22
- const colors = output.getColors();
22
+ let colors = output.getColors();
23
23
  try {
24
- output.info('Starting Vizzly authentication...');
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
- const verificationUri = deviceFlow.verification_uri || deviceFlow.verificationUri;
40
- const userCode = deviceFlow.user_code || deviceFlow.userCode;
41
- const deviceCode = deviceFlow.device_code || deviceFlow.deviceCode;
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
- const urlWithCode = `${verificationUri}?code=${userCode}`;
46
+ let urlWithCode = `${verificationUri}?code=${userCode}`;
48
47
 
49
- // Display user code prominently
50
- output.blank();
51
- output.print('='.repeat(50));
52
- output.blank();
53
- output.print(' Please visit the following URL to authorize this device:');
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
- const browserOpened = await openBrowser(urlWithCode);
56
+ let browserOpened = await openBrowser(urlWithCode);
66
57
  if (browserOpened) {
67
- output.info('Opening browser...');
58
+ output.complete('Browser opened');
68
59
  } else {
69
- output.warn('Could not open browser automatically. Please open the URL manually.');
60
+ output.warn('Could not open browser automatically');
61
+ output.hint('Please open the URL manually');
70
62
  }
71
63
  output.blank();
72
- output.info('After authorizing in your browser, press Enter to continue...');
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 status...');
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
- const tokensData = tokenData.tokens || tokenData;
108
- const tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
109
- const tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : tokenData.expires_at || tokenData.expiresAt;
110
- const tokens = {
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 message
120
- output.success('Successfully authenticated!');
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.info(`User: ${tokens.user.name || tokens.user.username}`);
126
- output.info(`Email: ${tokens.user.email}`);
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.info('Organizations:');
133
- for (const org of tokens.organizations) {
134
- output.print(` - ${org.name}${org.slug ? ` (@${org.slug})` : ''}`);
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
- const expiresAt = new Date(tokens.expiresAt);
142
- const msUntilExpiry = expiresAt.getTime() - Date.now();
143
- const daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
144
- const hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
145
- const minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
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.info(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleDateString()})`);
140
+ output.hint(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''}`);
148
141
  } else if (hoursUntilExpiry > 0) {
149
- output.info(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`);
142
+ output.hint(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`);
150
143
  } else if (minutesUntilExpiry > 0) {
151
- output.info(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
144
+ output.hint(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
152
145
  }
153
146
  }
154
147
  output.blank();
155
- output.info('You can now use Vizzly CLI commands without setting VIZZLY_TOKEN');
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.print('Please try logging in again.');
165
- output.print("If you don't have an account, sign up at https://vizzly.dev");
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.print('Please wait a few minutes before trying again.');
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);
@@ -20,9 +20,17 @@ export async function logoutCommand(options = {}, globalOptions = {}) {
20
20
  });
21
21
  try {
22
22
  // Check if user is logged in
23
- const auth = await getAuthTokens();
23
+ let auth = await getAuthTokens();
24
24
  if (!auth || !auth.accessToken) {
25
- output.info('You are not logged in');
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.info('Your authentication tokens have been cleared');
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) {