@vizzly-testing/cli 0.19.2 → 0.20.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) 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 +1 -1
  10. package/dist/client/index.js +0 -1
  11. package/dist/commands/doctor.js +3 -3
  12. package/dist/commands/finalize.js +41 -15
  13. package/dist/commands/login.js +7 -6
  14. package/dist/commands/logout.js +4 -4
  15. package/dist/commands/project.js +5 -4
  16. package/dist/commands/run.js +158 -90
  17. package/dist/commands/status.js +22 -18
  18. package/dist/commands/tdd.js +105 -78
  19. package/dist/commands/upload.js +61 -26
  20. package/dist/commands/whoami.js +4 -4
  21. package/dist/config/core.js +438 -0
  22. package/dist/config/index.js +13 -0
  23. package/dist/config/operations.js +327 -0
  24. package/dist/index.js +1 -1
  25. package/dist/project/core.js +295 -0
  26. package/dist/project/index.js +13 -0
  27. package/dist/project/operations.js +393 -0
  28. package/dist/report-generator/core.js +315 -0
  29. package/dist/report-generator/index.js +8 -0
  30. package/dist/report-generator/operations.js +196 -0
  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 +80 -48
  38. package/dist/server-manager/core.js +183 -0
  39. package/dist/server-manager/index.js +81 -0
  40. package/dist/server-manager/operations.js +208 -0
  41. package/dist/services/build-manager.js +2 -69
  42. package/dist/services/index.js +21 -48
  43. package/dist/services/screenshot-server.js +40 -74
  44. package/dist/services/server-manager.js +45 -80
  45. package/dist/services/static-report-generator.js +21 -163
  46. package/dist/services/test-runner.js +90 -249
  47. package/dist/services/uploader.js +56 -358
  48. package/dist/tdd/core/hotspot-coverage.js +112 -0
  49. package/dist/tdd/core/signature.js +101 -0
  50. package/dist/tdd/index.js +19 -0
  51. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  52. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  53. package/dist/tdd/services/baseline-downloader.js +151 -0
  54. package/dist/tdd/services/baseline-manager.js +166 -0
  55. package/dist/tdd/services/comparison-service.js +230 -0
  56. package/dist/tdd/services/hotspot-service.js +71 -0
  57. package/dist/tdd/services/result-service.js +123 -0
  58. package/dist/tdd/tdd-service.js +1081 -0
  59. package/dist/test-runner/core.js +255 -0
  60. package/dist/test-runner/index.js +13 -0
  61. package/dist/test-runner/operations.js +483 -0
  62. package/dist/types/client.d.ts +4 -2
  63. package/dist/types/index.d.ts +5 -0
  64. package/dist/uploader/core.js +396 -0
  65. package/dist/uploader/index.js +11 -0
  66. package/dist/uploader/operations.js +412 -0
  67. package/dist/utils/config-schema.js +8 -3
  68. package/package.json +7 -12
  69. package/dist/services/api-service.js +0 -412
  70. package/dist/services/auth-service.js +0 -226
  71. package/dist/services/config-service.js +0 -369
  72. package/dist/services/html-report-generator.js +0 -455
  73. package/dist/services/project-service.js +0 -326
  74. package/dist/services/report-generator/report.css +0 -411
  75. package/dist/services/report-generator/viewer.js +0 -102
  76. package/dist/services/tdd-service.js +0 -1429
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Auth Operations - Authentication operations with dependency injection
3
+ *
4
+ * Each operation takes its dependencies as parameters:
5
+ * - httpClient: for making HTTP requests
6
+ * - tokenStore: for reading/writing auth tokens
7
+ *
8
+ * This makes them trivially testable without mocking modules.
9
+ */
10
+
11
+ import { buildDevicePollPayload, buildLogoutPayload, buildRefreshPayload, buildTokenData, validateTokens } from './core.js';
12
+
13
+ // ============================================================================
14
+ // Device Flow Operations
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Initiate OAuth device flow
19
+ * @param {Object} httpClient - HTTP client with request method
20
+ * @returns {Promise<Object>} Device code, user code, verification URL
21
+ */
22
+ export async function initiateDeviceFlow(httpClient) {
23
+ return httpClient.request('/api/auth/cli/device/initiate', {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json'
27
+ }
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Poll for device authorization
33
+ * @param {Object} httpClient - HTTP client
34
+ * @param {string} deviceCode - Device code from initiate
35
+ * @returns {Promise<Object>} Token data or pending status
36
+ */
37
+ export async function pollDeviceAuthorization(httpClient, deviceCode) {
38
+ return httpClient.request('/api/auth/cli/device/poll', {
39
+ method: 'POST',
40
+ headers: {
41
+ 'Content-Type': 'application/json'
42
+ },
43
+ body: JSON.stringify(buildDevicePollPayload(deviceCode))
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Complete device flow and save tokens
49
+ * @param {Object} tokenStore - Token storage with saveTokens method
50
+ * @param {Object} tokenData - Token response from poll
51
+ * @returns {Promise<Object>} Token data
52
+ */
53
+ export async function completeDeviceFlow(tokenStore, tokenData) {
54
+ await tokenStore.saveTokens(buildTokenData(tokenData));
55
+ return tokenData;
56
+ }
57
+
58
+ // ============================================================================
59
+ // Token Operations
60
+ // ============================================================================
61
+
62
+ /**
63
+ * Refresh access token using refresh token
64
+ * @param {Object} httpClient - HTTP client
65
+ * @param {Object} tokenStore - Token storage
66
+ * @returns {Promise<Object>} New tokens
67
+ */
68
+ export async function refresh(httpClient, tokenStore) {
69
+ let auth = await tokenStore.getTokens();
70
+ let validation = validateTokens(auth, 'refreshToken');
71
+ if (!validation.valid) {
72
+ throw validation.error;
73
+ }
74
+ let response = await httpClient.request('/api/auth/cli/refresh', {
75
+ method: 'POST',
76
+ headers: {
77
+ 'Content-Type': 'application/json'
78
+ },
79
+ body: JSON.stringify(buildRefreshPayload(auth.refreshToken))
80
+ });
81
+
82
+ // Preserve existing user data when refreshing
83
+ await tokenStore.saveTokens(buildTokenData(response, auth.user));
84
+ return response;
85
+ }
86
+
87
+ // ============================================================================
88
+ // Logout Operations
89
+ // ============================================================================
90
+
91
+ /**
92
+ * Logout and revoke tokens
93
+ * @param {Object} httpClient - HTTP client
94
+ * @param {Object} tokenStore - Token storage
95
+ * @returns {Promise<void>}
96
+ */
97
+ export async function logout(httpClient, tokenStore) {
98
+ let auth = await tokenStore.getTokens();
99
+ if (auth?.refreshToken) {
100
+ try {
101
+ await httpClient.request('/api/auth/cli/logout', {
102
+ method: 'POST',
103
+ headers: {
104
+ 'Content-Type': 'application/json'
105
+ },
106
+ body: JSON.stringify(buildLogoutPayload(auth.refreshToken))
107
+ });
108
+ } catch (error) {
109
+ // If server request fails, still clear local tokens
110
+ console.warn('Warning: Failed to revoke tokens on server:', error.message);
111
+ }
112
+ }
113
+ await tokenStore.clearTokens();
114
+ }
115
+
116
+ // ============================================================================
117
+ // User Operations
118
+ // ============================================================================
119
+
120
+ /**
121
+ * Get current user information
122
+ * @param {Object} httpClient - HTTP client
123
+ * @param {Object} tokenStore - Token storage
124
+ * @returns {Promise<Object>} User and organization data
125
+ */
126
+ export async function whoami(httpClient, tokenStore) {
127
+ let auth = await tokenStore.getTokens();
128
+ let validation = validateTokens(auth, 'accessToken');
129
+ if (!validation.valid) {
130
+ throw validation.error;
131
+ }
132
+ return httpClient.authenticatedRequest('/api/auth/cli/whoami', auth.accessToken);
133
+ }
134
+
135
+ /**
136
+ * Check if user is authenticated
137
+ * @param {Object} httpClient - HTTP client
138
+ * @param {Object} tokenStore - Token storage
139
+ * @returns {Promise<boolean>} True if authenticated
140
+ */
141
+ export async function isAuthenticated(httpClient, tokenStore) {
142
+ try {
143
+ await whoami(httpClient, tokenStore);
144
+ return true;
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
package/dist/cli.js CHANGED
@@ -13,8 +13,8 @@ import { tddCommand, validateTddOptions } from './commands/tdd.js';
13
13
  import { runDaemonChild, tddStartCommand, tddStatusCommand, tddStopCommand } from './commands/tdd-daemon.js';
14
14
  import { uploadCommand, validateUploadOptions } from './commands/upload.js';
15
15
  import { validateWhoamiOptions, whoamiCommand } from './commands/whoami.js';
16
- import { loadPlugins } from './plugin-loader.js';
17
16
  import { createPluginServices } from './plugin-api.js';
17
+ import { loadPlugins } from './plugin-loader.js';
18
18
  import { createServices } from './services/index.js';
19
19
  import { loadConfig } from './utils/config-loader.js';
20
20
  import * as output from './utils/output.js';
@@ -120,7 +120,6 @@ function createSimpleClient(serverUrl) {
120
120
  name,
121
121
  image,
122
122
  properties: options,
123
- threshold: options.threshold || 0,
124
123
  fullPage: options.fullPage || false
125
124
  }),
126
125
  signal: controller.signal
@@ -1,6 +1,6 @@
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
5
  import { getApiToken } from '../utils/environment-config.js';
6
6
  import * as output from '../utils/output.js';
@@ -102,13 +102,13 @@ export async function doctorCommand(options = {}, globalOptions = {}) {
102
102
  } else {
103
103
  output.progress('Checking API connectivity...');
104
104
  try {
105
- const api = new ApiService({
105
+ let client = createApiClient({
106
106
  baseUrl: config.apiUrl,
107
107
  token: config.apiKey,
108
108
  command: 'doctor'
109
109
  });
110
110
  // Minimal, read-only call
111
- await api.getBuilds({
111
+ await getBuilds(client, {
112
112
  limit: 1
113
113
  });
114
114
  diagnostics.connectivity.ok = 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,14 +52,15 @@ 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 {
@@ -50,10 +68,18 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
50
68
  output.info(`Status: ${result.build.status}`);
51
69
  output.info(`Parallel ID: ${result.build.parallel_id}`);
52
70
  }
71
+ return {
72
+ success: true,
73
+ result
74
+ };
53
75
  } catch (error) {
54
76
  output.stopSpinner();
55
77
  output.error('Failed to finalize parallel build', error);
56
- process.exit(1);
78
+ exit(1);
79
+ return {
80
+ success: false,
81
+ error
82
+ };
57
83
  } finally {
58
84
  output.cleanup();
59
85
  }
@@ -65,7 +91,7 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
65
91
  * @param {Object} options - Command options
66
92
  */
67
93
  export function validateFinalizeOptions(parallelId, _options) {
68
- const errors = [];
94
+ let errors = [];
69
95
  if (!parallelId || parallelId.trim() === '') {
70
96
  errors.push('Parallel ID is required');
71
97
  }
@@ -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';
@@ -24,14 +24,15 @@ export async function loginCommand(options = {}, globalOptions = {}) {
24
24
  output.info('Starting Vizzly authentication...');
25
25
  output.blank();
26
26
 
27
- // Create auth service
28
- const authService = new AuthService({
27
+ // Create auth client and token store
28
+ let client = createAuthClient({
29
29
  baseUrl: options.apiUrl || getApiUrl()
30
30
  });
31
+ let tokenStore = createTokenStore();
31
32
 
32
33
  // Initiate device flow
33
34
  output.startSpinner('Connecting to Vizzly...');
34
- const deviceFlow = await authService.initiateDeviceFlow();
35
+ let deviceFlow = await initiateDeviceFlow(client);
35
36
  output.stopSpinner();
36
37
 
37
38
  // Handle both snake_case and camelCase field names
@@ -83,7 +84,7 @@ export async function loginCommand(options = {}, globalOptions = {}) {
83
84
 
84
85
  // Check authorization status
85
86
  output.startSpinner('Checking authorization status...');
86
- const pollResponse = await authService.pollDeviceAuthorization(deviceCode);
87
+ let pollResponse = await pollDeviceAuthorization(client, deviceCode);
87
88
  output.stopSpinner();
88
89
  let tokenData = null;
89
90
 
@@ -113,7 +114,7 @@ export async function loginCommand(options = {}, globalOptions = {}) {
113
114
  user: tokenData.user,
114
115
  organizations: tokenData.organizations
115
116
  };
116
- await authService.completeDeviceFlow(tokens);
117
+ await completeDeviceFlow(tokenStore, tokens);
117
118
 
118
119
  // Display success message
119
120
  output.success('Successfully authenticated!');
@@ -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
  /**
@@ -30,10 +29,11 @@ export async function logoutCommand(options = {}, globalOptions = {}) {
30
29
 
31
30
  // Logout
32
31
  output.startSpinner('Logging out...');
33
- const authService = new AuthService({
32
+ let client = createAuthClient({
34
33
  baseUrl: options.apiUrl || getApiUrl()
35
34
  });
36
- await authService.logout();
35
+ let tokenStore = createTokenStore();
36
+ await logout(client, tokenStore);
37
37
  output.stopSpinner();
38
38
  output.success('Successfully logged out');
39
39
  if (globalOptions.json) {
@@ -5,9 +5,9 @@
5
5
 
6
6
  import { resolve } from 'node:path';
7
7
  import readline from 'node:readline';
8
- import { AuthService } from '../services/auth-service.js';
8
+ import { createAuthClient, createTokenStore, getAuthTokens, whoami } from '../auth/index.js';
9
9
  import { getApiUrl } from '../utils/environment-config.js';
10
- import { deleteProjectMapping, getAuthTokens, getProjectMapping, getProjectMappings, saveProjectMapping } from '../utils/global-config.js';
10
+ import { deleteProjectMapping, getProjectMapping, getProjectMappings, saveProjectMapping } from '../utils/global-config.js';
11
11
  import * as output from '../utils/output.js';
12
12
 
13
13
  /**
@@ -30,13 +30,14 @@ export async function projectSelectCommand(options = {}, globalOptions = {}) {
30
30
  output.info('Run "vizzly login" to authenticate first');
31
31
  process.exit(1);
32
32
  }
33
- const authService = new AuthService({
33
+ let client = createAuthClient({
34
34
  baseUrl: options.apiUrl || getApiUrl()
35
35
  });
36
+ let tokenStore = createTokenStore();
36
37
 
37
38
  // Get user info to show organizations
38
39
  output.startSpinner('Fetching organizations...');
39
- const userInfo = await authService.whoami();
40
+ let userInfo = await whoami(client, tokenStore);
40
41
  output.stopSpinner();
41
42
  if (!userInfo.organizations || userInfo.organizations.length === 0) {
42
43
  output.error('No organizations found');