@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.
Files changed (84) hide show
  1. package/dist/api/client.js +134 -0
  2. package/dist/api/core.js +341 -0
  3. package/dist/api/endpoints.js +314 -0
  4. package/dist/api/index.js +19 -0
  5. package/dist/auth/client.js +91 -0
  6. package/dist/auth/core.js +176 -0
  7. package/dist/auth/index.js +30 -0
  8. package/dist/auth/operations.js +148 -0
  9. package/dist/cli.js +178 -3
  10. package/dist/client/index.js +144 -77
  11. package/dist/commands/doctor.js +121 -36
  12. package/dist/commands/finalize.js +49 -18
  13. package/dist/commands/init.js +13 -18
  14. package/dist/commands/login.js +49 -55
  15. package/dist/commands/logout.js +17 -9
  16. package/dist/commands/project.js +100 -71
  17. package/dist/commands/run.js +189 -95
  18. package/dist/commands/status.js +101 -66
  19. package/dist/commands/tdd-daemon.js +61 -32
  20. package/dist/commands/tdd.js +104 -98
  21. package/dist/commands/upload.js +78 -34
  22. package/dist/commands/whoami.js +44 -42
  23. package/dist/config/core.js +438 -0
  24. package/dist/config/index.js +13 -0
  25. package/dist/config/operations.js +327 -0
  26. package/dist/index.js +1 -1
  27. package/dist/project/core.js +295 -0
  28. package/dist/project/index.js +13 -0
  29. package/dist/project/operations.js +393 -0
  30. package/dist/reporter/reporter-bundle.css +1 -1
  31. package/dist/reporter/reporter-bundle.iife.js +16 -16
  32. package/dist/screenshot-server/core.js +157 -0
  33. package/dist/screenshot-server/index.js +11 -0
  34. package/dist/screenshot-server/operations.js +183 -0
  35. package/dist/sdk/index.js +3 -2
  36. package/dist/server/handlers/api-handler.js +14 -5
  37. package/dist/server/handlers/tdd-handler.js +191 -53
  38. package/dist/server/http-server.js +9 -3
  39. package/dist/server/routers/baseline.js +58 -0
  40. package/dist/server/routers/dashboard.js +10 -6
  41. package/dist/server/routers/screenshot.js +32 -0
  42. package/dist/server-manager/core.js +186 -0
  43. package/dist/server-manager/index.js +81 -0
  44. package/dist/server-manager/operations.js +209 -0
  45. package/dist/services/build-manager.js +2 -69
  46. package/dist/services/index.js +21 -48
  47. package/dist/services/screenshot-server.js +40 -74
  48. package/dist/services/server-manager.js +45 -80
  49. package/dist/services/test-runner.js +90 -250
  50. package/dist/services/uploader.js +56 -358
  51. package/dist/tdd/core/hotspot-coverage.js +112 -0
  52. package/dist/tdd/core/signature.js +101 -0
  53. package/dist/tdd/index.js +19 -0
  54. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  55. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  56. package/dist/tdd/services/baseline-downloader.js +151 -0
  57. package/dist/tdd/services/baseline-manager.js +166 -0
  58. package/dist/tdd/services/comparison-service.js +230 -0
  59. package/dist/tdd/services/hotspot-service.js +71 -0
  60. package/dist/tdd/services/result-service.js +123 -0
  61. package/dist/tdd/tdd-service.js +1145 -0
  62. package/dist/test-runner/core.js +255 -0
  63. package/dist/test-runner/index.js +13 -0
  64. package/dist/test-runner/operations.js +483 -0
  65. package/dist/types/client.d.ts +25 -2
  66. package/dist/uploader/core.js +396 -0
  67. package/dist/uploader/index.js +11 -0
  68. package/dist/uploader/operations.js +412 -0
  69. package/dist/utils/colors.js +187 -39
  70. package/dist/utils/config-loader.js +3 -6
  71. package/dist/utils/context.js +228 -0
  72. package/dist/utils/output.js +449 -14
  73. package/docs/api-reference.md +173 -8
  74. package/docs/tui-elements.md +560 -0
  75. package/package.json +13 -13
  76. package/dist/services/api-service.js +0 -412
  77. package/dist/services/auth-service.js +0 -226
  78. package/dist/services/config-service.js +0 -369
  79. package/dist/services/html-report-generator.js +0 -455
  80. package/dist/services/project-service.js +0 -326
  81. package/dist/services/report-generator/report.css +0 -411
  82. package/dist/services/report-generator/viewer.js +0 -102
  83. package/dist/services/static-report-generator.js +0 -207
  84. package/dist/services/tdd-service.js +0 -1437
@@ -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
- const diagnostics = {
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
- 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
- const api = new ApiService({
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 api.getBuilds({
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;
@@ -1,14 +1,27 @@
1
- import { createServices } from '../services/index.js';
2
- import { loadConfig } from '../utils/config-loader.js';
3
- import * as output from '../utils/output.js';
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
- const allOptions = {
32
+ let allOptions = {
20
33
  ...globalOptions,
21
34
  ...options
22
35
  };
23
- const config = await loadConfig(globalOptions.config, allOptions);
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
- process.exit(1);
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
- // Create services and get API service
55
+ // Call finalize endpoint via functional API
39
56
  output.startSpinner('Finalizing parallel build...');
40
- const services = createServices(config, 'finalize');
41
- const apiService = services.apiService;
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.success(`Parallel build ${result.build.id} finalized successfully`);
50
- output.info(`Status: ${result.build.status}`);
51
- 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
+ });
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
- process.exit(1);
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
- const errors = [];
99
+ let errors = [];
69
100
  if (!parallelId || parallelId.trim() === '') {
70
101
  errors.push('Parallel ID is required');
71
102
  }
@@ -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 {
@@ -3,7 +3,7 @@
3
3
  * Authenticates user via OAuth device flow
4
4
  */
5
5
 
6
- import { AuthService } from '../services/auth-service.js';
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
- 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
- // Create auth service
28
- const authService = new AuthService({
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
- const deviceFlow = await authService.initiateDeviceFlow();
34
+ let deviceFlow = await initiateDeviceFlow(client);
35
35
  output.stopSpinner();
36
36
 
37
37
  // Handle both snake_case and camelCase field names
38
- const verificationUri = deviceFlow.verification_uri || deviceFlow.verificationUri;
39
- const userCode = deviceFlow.user_code || deviceFlow.userCode;
40
- 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;
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
- const urlWithCode = `${verificationUri}?code=${userCode}`;
46
+ let urlWithCode = `${verificationUri}?code=${userCode}`;
47
47
 
48
- // Display user code prominently
49
- output.blank();
50
- output.print('='.repeat(50));
51
- output.blank();
52
- output.print(' Please visit the following URL to authorize this device:');
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
- const browserOpened = await openBrowser(urlWithCode);
56
+ let browserOpened = await openBrowser(urlWithCode);
65
57
  if (browserOpened) {
66
- output.info('Opening browser...');
58
+ output.complete('Browser opened');
67
59
  } else {
68
- 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');
69
62
  }
70
63
  output.blank();
71
- output.info('After authorizing in your browser, press Enter to continue...');
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 status...');
86
- const pollResponse = await authService.pollDeviceAuthorization(deviceCode);
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
- const tokensData = tokenData.tokens || tokenData;
107
- const tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
108
- const tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : tokenData.expires_at || tokenData.expiresAt;
109
- 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 = {
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 authService.completeDeviceFlow(tokens);
109
+ await completeDeviceFlow(tokenStore, tokens);
117
110
 
118
- // Display success message
119
- output.success('Successfully authenticated!');
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.info(`User: ${tokens.user.name || tokens.user.username}`);
125
- output.info(`Email: ${tokens.user.email}`);
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.info('Organizations:');
132
- for (const org of tokens.organizations) {
133
- output.print(` - ${org.name}${org.slug ? ` (@${org.slug})` : ''}`);
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
- const expiresAt = new Date(tokens.expiresAt);
141
- const msUntilExpiry = expiresAt.getTime() - Date.now();
142
- const daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
143
- const hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
144
- 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));
145
139
  if (daysUntilExpiry > 0) {
146
- output.info(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleDateString()})`);
140
+ output.hint(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''}`);
147
141
  } else if (hoursUntilExpiry > 0) {
148
- output.info(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`);
142
+ output.hint(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`);
149
143
  } else if (minutesUntilExpiry > 0) {
150
- output.info(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
144
+ output.hint(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
151
145
  }
152
146
  }
153
147
  output.blank();
154
- 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');
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.print('Please try logging in again.');
164
- 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");
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.print('Please wait a few minutes before trying again.');
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);
@@ -3,9 +3,8 @@
3
3
  * Clears stored authentication tokens
4
4
  */
5
5
 
6
- import { AuthService } from '../services/auth-service.js';
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
- const auth = await getAuthTokens();
23
+ let auth = await getAuthTokens();
25
24
  if (!auth || !auth.accessToken) {
26
- 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
+ }
27
34
  output.cleanup();
28
35
  return;
29
36
  }
30
37
 
31
38
  // Logout
32
39
  output.startSpinner('Logging out...');
33
- const authService = new AuthService({
40
+ let client = createAuthClient({
34
41
  baseUrl: options.apiUrl || getApiUrl()
35
42
  });
36
- await authService.logout();
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.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) {