@vizzly-testing/cli 0.20.0 → 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 (72) 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/commands/doctor.js +3 -3
  11. package/dist/commands/finalize.js +41 -15
  12. package/dist/commands/login.js +7 -6
  13. package/dist/commands/logout.js +4 -4
  14. package/dist/commands/project.js +5 -4
  15. package/dist/commands/run.js +158 -90
  16. package/dist/commands/status.js +22 -18
  17. package/dist/commands/tdd.js +105 -78
  18. package/dist/commands/upload.js +61 -26
  19. package/dist/commands/whoami.js +4 -4
  20. package/dist/config/core.js +438 -0
  21. package/dist/config/index.js +13 -0
  22. package/dist/config/operations.js +327 -0
  23. package/dist/index.js +1 -1
  24. package/dist/project/core.js +295 -0
  25. package/dist/project/index.js +13 -0
  26. package/dist/project/operations.js +393 -0
  27. package/dist/report-generator/core.js +315 -0
  28. package/dist/report-generator/index.js +8 -0
  29. package/dist/report-generator/operations.js +196 -0
  30. package/dist/reporter/reporter-bundle.iife.js +16 -16
  31. package/dist/screenshot-server/core.js +157 -0
  32. package/dist/screenshot-server/index.js +11 -0
  33. package/dist/screenshot-server/operations.js +183 -0
  34. package/dist/sdk/index.js +3 -2
  35. package/dist/server/handlers/api-handler.js +14 -5
  36. package/dist/server/handlers/tdd-handler.js +80 -48
  37. package/dist/server-manager/core.js +183 -0
  38. package/dist/server-manager/index.js +81 -0
  39. package/dist/server-manager/operations.js +208 -0
  40. package/dist/services/build-manager.js +2 -69
  41. package/dist/services/index.js +21 -48
  42. package/dist/services/screenshot-server.js +40 -74
  43. package/dist/services/server-manager.js +45 -80
  44. package/dist/services/static-report-generator.js +21 -163
  45. package/dist/services/test-runner.js +90 -250
  46. package/dist/services/uploader.js +56 -358
  47. package/dist/tdd/core/hotspot-coverage.js +112 -0
  48. package/dist/tdd/core/signature.js +101 -0
  49. package/dist/tdd/index.js +19 -0
  50. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  51. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  52. package/dist/tdd/services/baseline-downloader.js +151 -0
  53. package/dist/tdd/services/baseline-manager.js +166 -0
  54. package/dist/tdd/services/comparison-service.js +230 -0
  55. package/dist/tdd/services/hotspot-service.js +71 -0
  56. package/dist/tdd/services/result-service.js +123 -0
  57. package/dist/tdd/tdd-service.js +1081 -0
  58. package/dist/test-runner/core.js +255 -0
  59. package/dist/test-runner/index.js +13 -0
  60. package/dist/test-runner/operations.js +483 -0
  61. package/dist/uploader/core.js +396 -0
  62. package/dist/uploader/index.js +11 -0
  63. package/dist/uploader/operations.js +412 -0
  64. package/package.json +7 -12
  65. package/dist/services/api-service.js +0 -412
  66. package/dist/services/auth-service.js +0 -226
  67. package/dist/services/config-service.js +0 -369
  68. package/dist/services/html-report-generator.js +0 -455
  69. package/dist/services/project-service.js +0 -326
  70. package/dist/services/report-generator/report.css +0 -411
  71. package/dist/services/report-generator/viewer.js +0 -102
  72. package/dist/services/tdd-service.js +0 -1437
@@ -1,412 +0,0 @@
1
- /**
2
- * API Service for Vizzly
3
- * Handles HTTP requests to the Vizzly API
4
- */
5
-
6
- import crypto from 'node:crypto';
7
- import { URLSearchParams } from 'node:url';
8
- import { AuthError, VizzlyError } from '../errors/vizzly-error.js';
9
- import { getApiToken, getApiUrl, getUserAgent } from '../utils/environment-config.js';
10
- import { getAuthTokens, saveAuthTokens } from '../utils/global-config.js';
11
- import { getPackageVersion } from '../utils/package-info.js';
12
-
13
- /**
14
- * ApiService class for direct API communication
15
- */
16
- export class ApiService {
17
- constructor(options = {}) {
18
- // Accept config as-is, no fallbacks to environment
19
- // Config-loader handles all env/file resolution
20
- this.baseUrl = options.apiUrl || options.baseUrl || getApiUrl();
21
- this.token = options.apiKey || options.token || getApiToken(); // Accept both apiKey and token
22
- this.uploadAll = options.uploadAll || false;
23
-
24
- // Build User-Agent string
25
- const command = options.command || 'run';
26
- const baseUserAgent = `vizzly-cli/${getPackageVersion()} (${command})`;
27
- const sdkUserAgent = options.userAgent || getUserAgent();
28
- this.userAgent = sdkUserAgent ? `${baseUserAgent} ${sdkUserAgent}` : baseUserAgent;
29
- if (!this.token && !options.allowNoToken) {
30
- throw new VizzlyError('No API token provided. Set VIZZLY_TOKEN environment variable or link a project in the TDD dashboard.');
31
- }
32
- }
33
-
34
- /**
35
- * Make an API request
36
- * @param {string} endpoint - API endpoint
37
- * @param {Object} options - Fetch options
38
- * @param {boolean} isRetry - Internal flag to prevent infinite retry loops
39
- * @returns {Promise<Object>} Response data
40
- */
41
- async request(endpoint, options = {}, isRetry = false) {
42
- const url = `${this.baseUrl}${endpoint}`;
43
- const headers = {
44
- 'User-Agent': this.userAgent,
45
- ...options.headers
46
- };
47
- if (this.token) {
48
- headers.Authorization = `Bearer ${this.token}`;
49
- }
50
- const response = await fetch(url, {
51
- ...options,
52
- headers
53
- });
54
- if (!response.ok) {
55
- let errorText = '';
56
- try {
57
- if (typeof response.text === 'function') {
58
- errorText = await response.text();
59
- } else {
60
- errorText = response.statusText || '';
61
- }
62
- } catch {
63
- // ignore
64
- }
65
-
66
- // Handle authentication errors with automatic token refresh
67
- if (response.status === 401 && !isRetry) {
68
- // Attempt to refresh token if we have refresh token in global config
69
- const auth = await getAuthTokens();
70
- if (auth?.refreshToken) {
71
- try {
72
- // Attempt token refresh
73
- const refreshResponse = await fetch(`${this.baseUrl}/api/auth/cli/refresh`, {
74
- method: 'POST',
75
- headers: {
76
- 'Content-Type': 'application/json',
77
- 'User-Agent': this.userAgent
78
- },
79
- body: JSON.stringify({
80
- refreshToken: auth.refreshToken
81
- })
82
- });
83
- if (refreshResponse.ok) {
84
- const refreshData = await refreshResponse.json();
85
-
86
- // Save new tokens to global config
87
- await saveAuthTokens({
88
- accessToken: refreshData.accessToken,
89
- refreshToken: refreshData.refreshToken,
90
- expiresAt: refreshData.expiresAt,
91
- user: auth.user // Keep existing user data
92
- });
93
-
94
- // Update token for this service instance
95
- this.token = refreshData.accessToken;
96
-
97
- // Retry the original request with new token
98
- return this.request(endpoint, options, true);
99
- }
100
- } catch {
101
- // Token refresh failed, fall through to auth error
102
- }
103
- }
104
- throw new AuthError('Invalid or expired API token. Link a project via "vizzly project:select" or set VIZZLY_TOKEN.');
105
- }
106
- if (response.status === 401) {
107
- throw new AuthError('Invalid or expired API token. Link a project via "vizzly project:select" or set VIZZLY_TOKEN.');
108
- }
109
- throw new VizzlyError(`API request failed: ${response.status}${errorText ? ` - ${errorText}` : ''} (URL: ${url})`);
110
- }
111
- return response.json();
112
- }
113
-
114
- /**
115
- * Get build information
116
- * @param {string} buildId - Build ID
117
- * @param {string} include - Optional include parameter (e.g., 'screenshots')
118
- * @returns {Promise<Object>} Build data
119
- */
120
- async getBuild(buildId, include = null) {
121
- const endpoint = include ? `/api/sdk/builds/${buildId}?include=${include}` : `/api/sdk/builds/${buildId}`;
122
- return this.request(endpoint);
123
- }
124
-
125
- /**
126
- * Get TDD baselines for a build
127
- * Returns screenshots with pre-computed filenames for baseline download
128
- * @param {string} buildId - Build ID
129
- * @returns {Promise<Object>} { build, screenshots, signatureProperties }
130
- */
131
- async getTddBaselines(buildId) {
132
- return this.request(`/api/sdk/builds/${buildId}/tdd-baselines`);
133
- }
134
-
135
- /**
136
- * Get comparison information
137
- * @param {string} comparisonId - Comparison ID
138
- * @returns {Promise<Object>} Comparison data
139
- */
140
- async getComparison(comparisonId) {
141
- const response = await this.request(`/api/sdk/comparisons/${comparisonId}`);
142
- return response.comparison;
143
- }
144
-
145
- /**
146
- * Search for comparisons by name across builds
147
- * @param {string} name - Screenshot name to search for
148
- * @param {Object} filters - Optional filters (branch, limit, offset)
149
- * @param {string} [filters.branch] - Filter by branch name
150
- * @param {number} [filters.limit=50] - Maximum number of results (default: 50)
151
- * @param {number} [filters.offset=0] - Pagination offset (default: 0)
152
- * @returns {Promise<Object>} Search results with comparisons and pagination
153
- */
154
- async searchComparisons(name, filters = {}) {
155
- if (!name || typeof name !== 'string') {
156
- throw new VizzlyError('name is required and must be a non-empty string');
157
- }
158
- const {
159
- branch,
160
- limit = 50,
161
- offset = 0
162
- } = filters;
163
- const queryParams = new URLSearchParams({
164
- name,
165
- limit: String(limit),
166
- offset: String(offset)
167
- });
168
-
169
- // Only add branch if provided
170
- if (branch) queryParams.append('branch', branch);
171
- return this.request(`/api/sdk/comparisons/search?${queryParams}`);
172
- }
173
-
174
- /**
175
- * Get builds for a project
176
- * @param {Object} filters - Filter options
177
- * @returns {Promise<Array>} List of builds
178
- */
179
- async getBuilds(filters = {}) {
180
- const queryParams = new URLSearchParams(filters).toString();
181
- const endpoint = `/api/sdk/builds${queryParams ? `?${queryParams}` : ''}`;
182
- return this.request(endpoint);
183
- }
184
-
185
- /**
186
- * Create a new build
187
- * @param {Object} metadata - Build metadata
188
- * @returns {Promise<Object>} Created build data
189
- */
190
- async createBuild(metadata) {
191
- return this.request('/api/sdk/builds', {
192
- method: 'POST',
193
- headers: {
194
- 'Content-Type': 'application/json'
195
- },
196
- body: JSON.stringify({
197
- build: metadata
198
- })
199
- });
200
- }
201
-
202
- /**
203
- * Check if SHAs already exist on the server
204
- * @param {string[]|Object[]} shas - Array of SHA256 hashes to check, or array of screenshot objects with metadata
205
- * @param {string} buildId - Build ID for screenshot record creation
206
- * @returns {Promise<Object>} Response with existing SHAs and screenshot data
207
- */
208
- async checkShas(shas, buildId) {
209
- try {
210
- let requestBody;
211
-
212
- // Check if we're using the new signature-based format (array of objects) or legacy format (array of strings)
213
- if (Array.isArray(shas) && shas.length > 0 && typeof shas[0] === 'object' && shas[0].sha256) {
214
- // New signature-based format
215
- requestBody = {
216
- buildId,
217
- screenshots: shas
218
- };
219
- } else {
220
- // Legacy SHA-only format
221
- requestBody = {
222
- shas,
223
- buildId
224
- };
225
- }
226
- const response = await this.request('/api/sdk/check-shas', {
227
- method: 'POST',
228
- headers: {
229
- 'Content-Type': 'application/json'
230
- },
231
- body: JSON.stringify(requestBody)
232
- });
233
- return response;
234
- } catch (error) {
235
- // Continue without deduplication on error
236
- console.debug('SHA check failed, continuing without deduplication:', error.message);
237
- // Extract SHAs for fallback response regardless of format
238
- const shaList = Array.isArray(shas) && shas.length > 0 && typeof shas[0] === 'object' ? shas.map(s => s.sha256) : shas;
239
- return {
240
- existing: [],
241
- missing: shaList,
242
- screenshots: []
243
- };
244
- }
245
- }
246
-
247
- /**
248
- * Upload a screenshot with SHA checking
249
- * @param {string} buildId - Build ID
250
- * @param {string} name - Screenshot name
251
- * @param {Buffer} buffer - Screenshot data
252
- * @param {Object} metadata - Additional metadata
253
- * @returns {Promise<Object>} Upload result
254
- */
255
- async uploadScreenshot(buildId, name, buffer, metadata = {}) {
256
- // Skip SHA deduplication entirely if uploadAll flag is set
257
- if (this.uploadAll) {
258
- // Upload directly without SHA calculation or checking
259
- return this.request(`/api/sdk/builds/${buildId}/screenshots`, {
260
- method: 'POST',
261
- headers: {
262
- 'Content-Type': 'application/json'
263
- },
264
- body: JSON.stringify({
265
- name,
266
- image_data: buffer.toString('base64'),
267
- properties: metadata ?? {}
268
- // No SHA included when bypassing deduplication
269
- })
270
- });
271
- }
272
-
273
- // Normal flow with SHA deduplication using signature-based format
274
- const sha256 = crypto.createHash('sha256').update(buffer).digest('hex');
275
-
276
- // Create screenshot object with signature data for checking
277
- const screenshotCheck = [{
278
- sha256,
279
- name,
280
- browser: metadata?.browser || 'chrome',
281
- viewport_width: metadata?.viewport?.width || 1920,
282
- viewport_height: metadata?.viewport?.height || 1080
283
- }];
284
-
285
- // Check if this SHA with signature already exists
286
- const checkResult = await this.checkShas(screenshotCheck, buildId);
287
- if (checkResult.existing?.includes(sha256)) {
288
- // File already exists with same signature, screenshot record was automatically created
289
- const screenshot = checkResult.screenshots?.find(s => s.sha256 === sha256);
290
- return {
291
- message: 'Screenshot already exists, skipped upload',
292
- sha256,
293
- skipped: true,
294
- screenshot,
295
- fromExisting: true
296
- };
297
- }
298
-
299
- // File doesn't exist or has different signature, proceed with upload
300
- return this.request(`/api/sdk/builds/${buildId}/screenshots`, {
301
- method: 'POST',
302
- headers: {
303
- 'Content-Type': 'application/json'
304
- },
305
- body: JSON.stringify({
306
- name,
307
- image_data: buffer.toString('base64'),
308
- properties: metadata ?? {},
309
- sha256 // Include SHA for server-side deduplication
310
- })
311
- });
312
- }
313
-
314
- /**
315
- * Update build status
316
- * @param {string} buildId - Build ID
317
- * @param {string} status - Build status (pending|running|completed|failed)
318
- * @param {number} executionTimeMs - Execution time in milliseconds
319
- * @returns {Promise<Object>} Updated build data
320
- */
321
- async updateBuildStatus(buildId, status, executionTimeMs = null) {
322
- const body = {
323
- status
324
- };
325
- if (executionTimeMs !== null) {
326
- body.executionTimeMs = executionTimeMs;
327
- }
328
- return this.request(`/api/sdk/builds/${buildId}/status`, {
329
- method: 'PUT',
330
- headers: {
331
- 'Content-Type': 'application/json'
332
- },
333
- body: JSON.stringify(body)
334
- });
335
- }
336
-
337
- /**
338
- * Finalize a build (convenience method)
339
- * @param {string} buildId - Build ID
340
- * @param {boolean} success - Whether the build succeeded
341
- * @param {number} executionTimeMs - Execution time in milliseconds
342
- * @returns {Promise<Object>} Finalized build data
343
- */
344
- async finalizeBuild(buildId, success = true, executionTimeMs = null) {
345
- const status = success ? 'completed' : 'failed';
346
- return this.updateBuildStatus(buildId, status, executionTimeMs);
347
- }
348
-
349
- /**
350
- * Get token context (organization and project info)
351
- * @returns {Promise<Object>} Token context data
352
- */
353
- async getTokenContext() {
354
- return this.request('/api/sdk/token/context');
355
- }
356
-
357
- /**
358
- * Finalize a parallel build
359
- * @param {string} parallelId - Parallel ID to finalize
360
- * @returns {Promise<Object>} Finalization result
361
- */
362
- async finalizeParallelBuild(parallelId) {
363
- return this.request(`/api/sdk/parallel/${parallelId}/finalize`, {
364
- method: 'POST',
365
- headers: {
366
- 'Content-Type': 'application/json'
367
- }
368
- });
369
- }
370
-
371
- /**
372
- * Get hotspot analysis for a single screenshot
373
- * @param {string} screenshotName - Screenshot name to get hotspots for
374
- * @param {Object} options - Optional settings
375
- * @param {number} [options.windowSize=20] - Number of historical builds to analyze
376
- * @returns {Promise<Object>} Hotspot analysis data
377
- */
378
- async getScreenshotHotspots(screenshotName, options = {}) {
379
- const {
380
- windowSize = 20
381
- } = options;
382
- const queryParams = new URLSearchParams({
383
- windowSize: String(windowSize)
384
- });
385
- const encodedName = encodeURIComponent(screenshotName);
386
- return this.request(`/api/sdk/screenshots/${encodedName}/hotspots?${queryParams}`);
387
- }
388
-
389
- /**
390
- * Batch get hotspot analysis for multiple screenshots
391
- * More efficient than calling getScreenshotHotspots for each screenshot
392
- * @param {string[]} screenshotNames - Array of screenshot names
393
- * @param {Object} options - Optional settings
394
- * @param {number} [options.windowSize=20] - Number of historical builds to analyze
395
- * @returns {Promise<Object>} Hotspots keyed by screenshot name
396
- */
397
- async getBatchHotspots(screenshotNames, options = {}) {
398
- const {
399
- windowSize = 20
400
- } = options;
401
- return this.request('/api/sdk/screenshots/hotspots', {
402
- method: 'POST',
403
- headers: {
404
- 'Content-Type': 'application/json'
405
- },
406
- body: JSON.stringify({
407
- screenshot_names: screenshotNames,
408
- windowSize
409
- })
410
- });
411
- }
412
- }
@@ -1,226 +0,0 @@
1
- /**
2
- * Authentication Service for Vizzly CLI
3
- * Handles authentication flows with the Vizzly API
4
- */
5
-
6
- import { AuthError, VizzlyError } from '../errors/vizzly-error.js';
7
- import { getApiUrl } from '../utils/environment-config.js';
8
- import { clearAuthTokens, getAuthTokens, saveAuthTokens } from '../utils/global-config.js';
9
- import { getPackageVersion } from '../utils/package-info.js';
10
-
11
- /**
12
- * AuthService class for CLI authentication
13
- */
14
- export class AuthService {
15
- constructor(options = {}) {
16
- this.baseUrl = options.baseUrl || getApiUrl();
17
- this.userAgent = `vizzly-cli/${getPackageVersion()} (auth)`;
18
- }
19
-
20
- /**
21
- * Make an unauthenticated API request
22
- * @param {string} endpoint - API endpoint
23
- * @param {Object} options - Fetch options
24
- * @returns {Promise<Object>} Response data
25
- */
26
- async request(endpoint, options = {}) {
27
- const url = `${this.baseUrl}${endpoint}`;
28
- const headers = {
29
- 'User-Agent': this.userAgent,
30
- ...options.headers
31
- };
32
- const response = await fetch(url, {
33
- ...options,
34
- headers
35
- });
36
- if (!response.ok) {
37
- let errorText = '';
38
- let errorData = null;
39
- try {
40
- const contentType = response.headers.get('content-type');
41
- if (contentType?.includes('application/json')) {
42
- errorData = await response.json();
43
- errorText = errorData.error || errorData.message || '';
44
- } else {
45
- errorText = await response.text();
46
- }
47
- } catch {
48
- errorText = response.statusText || '';
49
- }
50
- if (response.status === 401) {
51
- throw new AuthError(errorText || 'Invalid credentials. Please check your email/username and password.');
52
- }
53
- if (response.status === 429) {
54
- throw new VizzlyError('Too many login attempts. Please try again later.', 'RATE_LIMIT_ERROR');
55
- }
56
- throw new VizzlyError(`Authentication request failed: ${response.status}${errorText ? ` - ${errorText}` : ''}`, 'AUTH_REQUEST_ERROR');
57
- }
58
- return response.json();
59
- }
60
-
61
- /**
62
- * Make an authenticated API request
63
- * @param {string} endpoint - API endpoint
64
- * @param {Object} options - Fetch options
65
- * @returns {Promise<Object>} Response data
66
- */
67
- async authenticatedRequest(endpoint, options = {}) {
68
- const auth = await getAuthTokens();
69
- if (!auth || !auth.accessToken) {
70
- throw new AuthError('No authentication token found. Please run "vizzly login" first.');
71
- }
72
- const url = `${this.baseUrl}${endpoint}`;
73
- const headers = {
74
- 'User-Agent': this.userAgent,
75
- Authorization: `Bearer ${auth.accessToken}`,
76
- ...options.headers
77
- };
78
- const response = await fetch(url, {
79
- ...options,
80
- headers
81
- });
82
- if (!response.ok) {
83
- let errorText = '';
84
- try {
85
- const contentType = response.headers.get('content-type');
86
- if (contentType?.includes('application/json')) {
87
- const errorData = await response.json();
88
- errorText = errorData.error || errorData.message || '';
89
- } else {
90
- errorText = await response.text();
91
- }
92
- } catch {
93
- errorText = response.statusText || '';
94
- }
95
- if (response.status === 401) {
96
- throw new AuthError('Authentication token is invalid or expired. Please run "vizzly login" again.');
97
- }
98
- throw new VizzlyError(`API request failed: ${response.status}${errorText ? ` - ${errorText}` : ''} (${endpoint})`, 'API_REQUEST_ERROR');
99
- }
100
- return response.json();
101
- }
102
-
103
- /**
104
- * Initiate OAuth device flow
105
- * @returns {Promise<Object>} Device code, user code, verification URL
106
- */
107
- async initiateDeviceFlow() {
108
- return this.request('/api/auth/cli/device/initiate', {
109
- method: 'POST',
110
- headers: {
111
- 'Content-Type': 'application/json'
112
- }
113
- });
114
- }
115
-
116
- /**
117
- * Poll for device authorization
118
- * @param {string} deviceCode - Device code from initiate
119
- * @returns {Promise<Object>} Token data or pending status
120
- */
121
- async pollDeviceAuthorization(deviceCode) {
122
- return this.request('/api/auth/cli/device/poll', {
123
- method: 'POST',
124
- headers: {
125
- 'Content-Type': 'application/json'
126
- },
127
- body: JSON.stringify({
128
- device_code: deviceCode
129
- })
130
- });
131
- }
132
-
133
- /**
134
- * Complete device flow and save tokens
135
- * @param {Object} tokenData - Token response from poll
136
- * @returns {Promise<Object>} Token data with user info
137
- */
138
- async completeDeviceFlow(tokenData) {
139
- // Save tokens to global config
140
- await saveAuthTokens({
141
- accessToken: tokenData.accessToken,
142
- refreshToken: tokenData.refreshToken,
143
- expiresAt: tokenData.expiresAt,
144
- user: tokenData.user
145
- });
146
- return tokenData;
147
- }
148
-
149
- /**
150
- * Refresh access token using refresh token
151
- * @returns {Promise<Object>} New tokens
152
- */
153
- async refresh() {
154
- const auth = await getAuthTokens();
155
- if (!auth || !auth.refreshToken) {
156
- throw new AuthError('No refresh token found. Please run "vizzly login" first.');
157
- }
158
- const response = await this.request('/api/auth/cli/refresh', {
159
- method: 'POST',
160
- headers: {
161
- 'Content-Type': 'application/json'
162
- },
163
- body: JSON.stringify({
164
- refreshToken: auth.refreshToken
165
- })
166
- });
167
-
168
- // Update tokens in global config
169
- await saveAuthTokens({
170
- accessToken: response.accessToken,
171
- refreshToken: response.refreshToken,
172
- expiresAt: response.expiresAt,
173
- user: auth.user // Keep existing user data
174
- });
175
- return response;
176
- }
177
-
178
- /**
179
- * Logout and revoke tokens
180
- * @returns {Promise<void>}
181
- */
182
- async logout() {
183
- const auth = await getAuthTokens();
184
- if (auth?.refreshToken) {
185
- try {
186
- // Attempt to revoke tokens on server
187
- await this.request('/api/auth/cli/logout', {
188
- method: 'POST',
189
- headers: {
190
- 'Content-Type': 'application/json'
191
- },
192
- body: JSON.stringify({
193
- refreshToken: auth.refreshToken
194
- })
195
- });
196
- } catch (error) {
197
- // If server request fails, still clear local tokens
198
- console.warn('Warning: Failed to revoke tokens on server:', error.message);
199
- }
200
- }
201
-
202
- // Clear tokens from global config
203
- await clearAuthTokens();
204
- }
205
-
206
- /**
207
- * Get current user information
208
- * @returns {Promise<Object>} User and organization data
209
- */
210
- async whoami() {
211
- return this.authenticatedRequest('/api/auth/cli/whoami');
212
- }
213
-
214
- /**
215
- * Check if user is authenticated
216
- * @returns {Promise<boolean>} True if authenticated
217
- */
218
- async isAuthenticated() {
219
- try {
220
- await this.whoami();
221
- return true;
222
- } catch {
223
- return false;
224
- }
225
- }
226
- }