@vizzly-testing/cli 0.10.3 → 0.11.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 (47) hide show
  1. package/README.md +168 -8
  2. package/claude-plugin/.claude-plugin/.mcp.json +8 -0
  3. package/claude-plugin/.claude-plugin/README.md +114 -0
  4. package/claude-plugin/.claude-plugin/marketplace.json +28 -0
  5. package/claude-plugin/.claude-plugin/plugin.json +14 -0
  6. package/claude-plugin/commands/debug-diff.md +153 -0
  7. package/claude-plugin/commands/setup.md +137 -0
  8. package/claude-plugin/commands/suggest-screenshots.md +111 -0
  9. package/claude-plugin/commands/tdd-status.md +43 -0
  10. package/claude-plugin/mcp/vizzly-server/cloud-api-provider.js +354 -0
  11. package/claude-plugin/mcp/vizzly-server/index.js +861 -0
  12. package/claude-plugin/mcp/vizzly-server/local-tdd-provider.js +422 -0
  13. package/claude-plugin/mcp/vizzly-server/token-resolver.js +185 -0
  14. package/dist/cli.js +64 -0
  15. package/dist/client/index.js +13 -3
  16. package/dist/commands/login.js +195 -0
  17. package/dist/commands/logout.js +71 -0
  18. package/dist/commands/project.js +351 -0
  19. package/dist/commands/run.js +30 -0
  20. package/dist/commands/whoami.js +162 -0
  21. package/dist/plugin-loader.js +9 -15
  22. package/dist/sdk/index.js +16 -4
  23. package/dist/services/api-service.js +50 -7
  24. package/dist/services/auth-service.js +226 -0
  25. package/dist/types/client/index.d.ts +9 -3
  26. package/dist/types/commands/login.d.ts +11 -0
  27. package/dist/types/commands/logout.d.ts +11 -0
  28. package/dist/types/commands/project.d.ts +28 -0
  29. package/dist/types/commands/whoami.d.ts +11 -0
  30. package/dist/types/sdk/index.d.ts +9 -4
  31. package/dist/types/services/api-service.d.ts +2 -1
  32. package/dist/types/services/auth-service.d.ts +59 -0
  33. package/dist/types/utils/browser.d.ts +6 -0
  34. package/dist/types/utils/config-loader.d.ts +1 -1
  35. package/dist/types/utils/config-schema.d.ts +8 -174
  36. package/dist/types/utils/file-helpers.d.ts +18 -0
  37. package/dist/types/utils/global-config.d.ts +84 -0
  38. package/dist/utils/browser.js +44 -0
  39. package/dist/utils/config-loader.js +69 -3
  40. package/dist/utils/file-helpers.js +64 -0
  41. package/dist/utils/global-config.js +259 -0
  42. package/docs/api-reference.md +177 -6
  43. package/docs/authentication.md +334 -0
  44. package/docs/getting-started.md +21 -2
  45. package/docs/plugins.md +27 -0
  46. package/docs/test-integration.md +60 -10
  47. package/package.json +5 -3
@@ -0,0 +1,422 @@
1
+ import { readFile, readdir, stat, access, copyFile, mkdir, writeFile } from 'fs/promises';
2
+ import { join, resolve } from 'path';
3
+ import { constants } from 'fs';
4
+
5
+ /**
6
+ * Provider for reading local TDD state from .vizzly directory
7
+ */
8
+ export class LocalTDDProvider {
9
+ /**
10
+ * Find .vizzly directory by searching up from current directory
11
+ */
12
+ async findVizzlyDir(workingDirectory = process.cwd()) {
13
+ let currentDir = resolve(workingDirectory);
14
+ let maxDepth = 10;
15
+ let depth = 0;
16
+
17
+ while (depth < maxDepth) {
18
+ let vizzlyDir = join(currentDir, '.vizzly');
19
+ try {
20
+ await access(vizzlyDir, constants.R_OK);
21
+ return vizzlyDir;
22
+ } catch {
23
+ // Directory doesn't exist or isn't readable, go up one level
24
+ let parentDir = join(currentDir, '..');
25
+ if (parentDir === currentDir) {
26
+ // Reached root
27
+ break;
28
+ }
29
+ currentDir = parentDir;
30
+ depth++;
31
+ }
32
+ }
33
+
34
+ return null;
35
+ }
36
+
37
+ /**
38
+ * Get baseline metadata if available
39
+ * Returns metadata about cloud build that baselines were downloaded from
40
+ */
41
+ async getBaselineMetadata(workingDirectory) {
42
+ let vizzlyDir = await this.findVizzlyDir(workingDirectory);
43
+ if (!vizzlyDir) {
44
+ return null;
45
+ }
46
+
47
+ let metadataPath = join(vizzlyDir, 'baseline-metadata.json');
48
+ try {
49
+ let content = await readFile(metadataPath, 'utf-8');
50
+ return JSON.parse(content);
51
+ } catch {
52
+ // No metadata file exists (expected for local-only baselines)
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Get TDD server information from server.json
59
+ */
60
+ async getServerInfo(workingDirectory) {
61
+ let vizzlyDir = await this.findVizzlyDir(workingDirectory);
62
+ if (!vizzlyDir) {
63
+ return {
64
+ running: false,
65
+ message: 'No .vizzly directory found. TDD server not running.'
66
+ };
67
+ }
68
+
69
+ let serverJsonPath = join(vizzlyDir, 'server.json');
70
+ try {
71
+ let content = await readFile(serverJsonPath, 'utf-8');
72
+ let serverInfo = JSON.parse(content);
73
+ return {
74
+ running: true,
75
+ ...serverInfo,
76
+ dashboardUrl: `http://localhost:${serverInfo.port}/dashboard`
77
+ };
78
+ } catch {
79
+ return {
80
+ running: false,
81
+ message: 'TDD server not running or server.json not found',
82
+ vizzlyDir
83
+ };
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Get current TDD status with comparison results
89
+ */
90
+ async getTDDStatus(workingDirectory) {
91
+ let vizzlyDir = await this.findVizzlyDir(workingDirectory);
92
+ if (!vizzlyDir) {
93
+ return {
94
+ error: 'No .vizzly directory found',
95
+ message: 'Run `vizzly tdd start` or `vizzly tdd run "npm test"` to initialize TDD mode'
96
+ };
97
+ }
98
+
99
+ let serverInfo = await this.getServerInfo(workingDirectory);
100
+
101
+ // Read comparison results from report data
102
+ let reportDataPath = join(vizzlyDir, 'report-data.json');
103
+ let comparisons = [];
104
+ let summary = {
105
+ total: 0,
106
+ passed: 0,
107
+ failed: 0,
108
+ new: 0
109
+ };
110
+
111
+ try {
112
+ let reportData = await readFile(reportDataPath, 'utf-8');
113
+ let data = JSON.parse(reportData);
114
+ comparisons = data.comparisons || [];
115
+
116
+ // Calculate summary
117
+ summary.total = comparisons.length;
118
+ summary.passed = comparisons.filter((c) => c.status === 'passed').length;
119
+ summary.failed = comparisons.filter((c) => c.status === 'failed').length;
120
+ summary.new = comparisons.filter((c) => c.status === 'new').length;
121
+ } catch {
122
+ // No comparisons yet
123
+ }
124
+
125
+ // List available diff images
126
+ let diffsDir = join(vizzlyDir, 'diffs');
127
+ let diffImages = [];
128
+ try {
129
+ let files = await readdir(diffsDir);
130
+ diffImages = files
131
+ .filter((f) => f.endsWith('.png'))
132
+ .map((f) => ({
133
+ name: f.replace('.png', ''),
134
+ path: join(diffsDir, f)
135
+ }));
136
+ } catch {
137
+ // No diffs directory
138
+ }
139
+
140
+ // Get baseline metadata if available
141
+ let baselineMetadata = await this.getBaselineMetadata(workingDirectory);
142
+
143
+ return {
144
+ vizzlyDir,
145
+ serverInfo,
146
+ summary,
147
+ comparisons: comparisons.map((c) => {
148
+ // Convert paths from report-data.json to filesystem paths
149
+ // Report paths like "/images/baselines/foo.png" -> ".vizzly/baselines/foo.png"
150
+ let makeFilesystemPath = (path) => {
151
+ if (!path) return null;
152
+ // Strip /images/ prefix and join with vizzlyDir
153
+ let cleanPath = path.replace(/^\/images\//, '');
154
+ return join(vizzlyDir, cleanPath);
155
+ };
156
+
157
+ return {
158
+ name: c.name,
159
+ status: c.status,
160
+ diffPercentage: c.diffPercentage,
161
+ threshold: c.threshold,
162
+ hasDiff: c.diffPercentage > c.threshold,
163
+ currentPath: makeFilesystemPath(c.current),
164
+ baselinePath: makeFilesystemPath(c.baseline),
165
+ diffPath: makeFilesystemPath(c.diff)
166
+ };
167
+ }),
168
+ diffImages,
169
+ failedComparisons: comparisons.filter((c) => c.status === 'failed').map((c) => c.name),
170
+ newScreenshots: comparisons.filter((c) => c.status === 'new').map((c) => c.name),
171
+ baselineMetadata
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Get detailed information about a specific comparison
177
+ */
178
+ async getComparisonDetails(screenshotName, workingDirectory) {
179
+ let status = await this.getTDDStatus(workingDirectory);
180
+ if (status.error) {
181
+ return status;
182
+ }
183
+
184
+ let comparison = status.comparisons.find((c) => c.name === screenshotName);
185
+ if (!comparison) {
186
+ return {
187
+ error: `Screenshot "${screenshotName}" not found`,
188
+ availableScreenshots: status.comparisons.map((c) => c.name)
189
+ };
190
+ }
191
+
192
+ return {
193
+ ...comparison,
194
+ mode: 'local',
195
+ vizzlyDir: status.vizzlyDir,
196
+ baselineMetadata: status.baselineMetadata,
197
+ analysis: this.analyzeComparison(comparison)
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Analyze comparison to provide helpful insights
203
+ */
204
+ analyzeComparison(comparison) {
205
+ let insights = [];
206
+
207
+ if (comparison.status === 'new') {
208
+ insights.push('This is a new screenshot with no baseline for comparison.');
209
+ insights.push('Accept this screenshot as the baseline if it looks correct.');
210
+ } else if (comparison.status === 'failed') {
211
+ let diffPct = comparison.diffPercentage;
212
+ if (diffPct < 1) {
213
+ insights.push(
214
+ `Small difference detected (${diffPct.toFixed(2)}%). This might be minor anti-aliasing or subpixel rendering.`
215
+ );
216
+ } else if (diffPct < 5) {
217
+ insights.push(
218
+ `Moderate difference (${diffPct.toFixed(2)}%). Likely a layout shift or color change.`
219
+ );
220
+ } else {
221
+ insights.push(
222
+ `Large difference (${diffPct.toFixed(2)}%). Significant visual change detected.`
223
+ );
224
+ }
225
+
226
+ insights.push(
227
+ 'Use the Read tool to view the baseline and current image paths to identify the differences.'
228
+ );
229
+ insights.push('Do NOT attempt to read the diff image path as it may cause API errors.');
230
+ insights.push('If this change is intentional, accept it as the new baseline.');
231
+ insights.push('If unintentional, investigate and fix the visual issue.');
232
+ } else if (comparison.status === 'passed') {
233
+ insights.push('Screenshot matches the baseline within threshold.');
234
+ }
235
+
236
+ return insights;
237
+ }
238
+
239
+ /**
240
+ * List all diff images
241
+ */
242
+ async listDiffImages(workingDirectory) {
243
+ let vizzlyDir = await this.findVizzlyDir(workingDirectory);
244
+ if (!vizzlyDir) {
245
+ return {
246
+ error: 'No .vizzly directory found'
247
+ };
248
+ }
249
+
250
+ let diffsDir = join(vizzlyDir, 'diffs');
251
+ try {
252
+ let files = await readdir(diffsDir);
253
+ let diffImages = [];
254
+
255
+ for (let file of files) {
256
+ if (!file.endsWith('.png')) continue;
257
+
258
+ let filePath = join(diffsDir, file);
259
+ let stats = await stat(filePath);
260
+
261
+ diffImages.push({
262
+ name: file.replace('.png', ''),
263
+ path: filePath,
264
+ size: stats.size,
265
+ modified: stats.mtime
266
+ });
267
+ }
268
+
269
+ return {
270
+ count: diffImages.length,
271
+ diffImages: diffImages.sort((a, b) => b.modified.getTime() - a.modified.getTime())
272
+ };
273
+ } catch {
274
+ return {
275
+ count: 0,
276
+ diffImages: [],
277
+ message: 'No diff images found'
278
+ };
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Accept a screenshot as new baseline
284
+ * Copies current screenshot to baselines directory
285
+ */
286
+ async acceptBaseline(screenshotName, workingDirectory) {
287
+ let vizzlyDir = await this.findVizzlyDir(workingDirectory);
288
+ if (!vizzlyDir) {
289
+ throw new Error('No .vizzly directory found. TDD server not running.');
290
+ }
291
+
292
+ let currentPath = join(vizzlyDir, 'screenshots', `${screenshotName}.png`);
293
+ let baselinePath = join(vizzlyDir, 'baselines', `${screenshotName}.png`);
294
+
295
+ try {
296
+ // Check current screenshot exists
297
+ await access(currentPath, constants.R_OK);
298
+
299
+ // Ensure baselines directory exists
300
+ await mkdir(join(vizzlyDir, 'baselines'), { recursive: true });
301
+
302
+ // Copy current to baseline
303
+ await copyFile(currentPath, baselinePath);
304
+
305
+ return {
306
+ success: true,
307
+ message: `Accepted ${screenshotName} as new baseline`,
308
+ screenshotName,
309
+ baselinePath
310
+ };
311
+ } catch (error) {
312
+ throw new Error(
313
+ `Failed to accept baseline: ${error.message}. Make sure the screenshot exists at ${currentPath}`
314
+ );
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Reject a screenshot (marks it for investigation)
320
+ * Creates a rejection marker file
321
+ */
322
+ async rejectBaseline(screenshotName, reason, workingDirectory) {
323
+ let vizzlyDir = await this.findVizzlyDir(workingDirectory);
324
+ if (!vizzlyDir) {
325
+ throw new Error('No .vizzly directory found. TDD server not running.');
326
+ }
327
+
328
+ let rejectionsDir = join(vizzlyDir, 'rejections');
329
+ await mkdir(rejectionsDir, { recursive: true });
330
+
331
+ let rejectionFile = join(rejectionsDir, `${screenshotName}.json`);
332
+ let rejectionData = {
333
+ screenshotName,
334
+ reason,
335
+ rejectedAt: new Date().toISOString()
336
+ };
337
+
338
+ try {
339
+ await writeFile(rejectionFile, JSON.stringify(rejectionData, null, 2));
340
+
341
+ return {
342
+ success: true,
343
+ message: `Rejected ${screenshotName}: ${reason}`,
344
+ screenshotName,
345
+ reason
346
+ };
347
+ } catch (error) {
348
+ throw new Error(`Failed to reject baseline: ${error.message}`);
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Download and save baselines from cloud build
354
+ */
355
+ async downloadBaselinesFromCloud(screenshots, workingDirectory, buildMetadata = null) {
356
+ let vizzlyDir = await this.findVizzlyDir(workingDirectory);
357
+ if (!vizzlyDir) {
358
+ throw new Error('No .vizzly directory found. TDD server not running.');
359
+ }
360
+
361
+ let baselinesDir = join(vizzlyDir, 'baselines');
362
+ await mkdir(baselinesDir, { recursive: true });
363
+
364
+ let results = [];
365
+
366
+ for (let screenshot of screenshots) {
367
+ try {
368
+ // Download image from URL
369
+ let response = await fetch(screenshot.url);
370
+ if (!response.ok) {
371
+ throw new Error(`HTTP ${response.status}`);
372
+ }
373
+
374
+ // eslint-disable-next-line no-undef
375
+ let buffer = Buffer.from(await response.arrayBuffer());
376
+ let baselinePath = join(baselinesDir, `${screenshot.name}.png`);
377
+
378
+ await writeFile(baselinePath, buffer);
379
+
380
+ results.push({
381
+ name: screenshot.name,
382
+ success: true,
383
+ path: baselinePath
384
+ });
385
+ } catch (error) {
386
+ results.push({
387
+ name: screenshot.name,
388
+ success: false,
389
+ error: error.message
390
+ });
391
+ }
392
+ }
393
+
394
+ let successCount = results.filter((r) => r.success).length;
395
+
396
+ // Save baseline metadata if build metadata provided
397
+ if (buildMetadata && successCount > 0) {
398
+ let metadata = {
399
+ sourceType: 'cloud-build',
400
+ buildId: buildMetadata.id,
401
+ buildName: buildMetadata.name,
402
+ branch: buildMetadata.branch,
403
+ commitSha: buildMetadata.commitSha,
404
+ commitMessage: buildMetadata.commitMessage,
405
+ commonAncestorSha: buildMetadata.commonAncestorSha,
406
+ buildUrl: buildMetadata.url,
407
+ downloadedAt: new Date().toISOString(),
408
+ screenshots: results.filter((r) => r.success).map((r) => r.name)
409
+ };
410
+
411
+ let metadataPath = join(vizzlyDir, 'baseline-metadata.json');
412
+ await writeFile(metadataPath, JSON.stringify(metadata, null, 2));
413
+ }
414
+
415
+ return {
416
+ success: successCount > 0,
417
+ message: `Downloaded ${successCount}/${screenshots.length} baselines`,
418
+ results,
419
+ metadataSaved: buildMetadata && successCount > 0
420
+ };
421
+ }
422
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Token Resolution for MCP Server
3
+ * Resolves API tokens with the same priority as the CLI
4
+ */
5
+
6
+ import { homedir } from 'os';
7
+ import { join, dirname, parse } from 'path';
8
+ import { readFile } from 'fs/promises';
9
+ import { existsSync } from 'fs';
10
+
11
+ /**
12
+ * Get the path to the global config file
13
+ */
14
+ function getGlobalConfigPath() {
15
+ return join(homedir(), '.vizzly', 'config.json');
16
+ }
17
+
18
+ /**
19
+ * Load the global configuration
20
+ */
21
+ async function loadGlobalConfig() {
22
+ try {
23
+ let configPath = getGlobalConfigPath();
24
+
25
+ if (!existsSync(configPath)) {
26
+ return {};
27
+ }
28
+
29
+ let content = await readFile(configPath, 'utf-8');
30
+ return JSON.parse(content);
31
+ } catch (error) {
32
+ // If file doesn't exist or is corrupted, return empty config
33
+ if (error.code === 'ENOENT') {
34
+ return {};
35
+ }
36
+
37
+ // Log warning about corrupted config but don't crash
38
+ console.error('Warning: Global config file is corrupted, ignoring');
39
+ return {};
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Get authentication tokens from global config
45
+ */
46
+ async function getAuthTokens() {
47
+ let config = await loadGlobalConfig();
48
+
49
+ if (!config.auth || !config.auth.accessToken) {
50
+ return null;
51
+ }
52
+
53
+ return config.auth;
54
+ }
55
+
56
+ /**
57
+ * Get the access token from global config if available
58
+ */
59
+ async function getAccessToken() {
60
+ let auth = await getAuthTokens();
61
+ return auth?.accessToken || null;
62
+ }
63
+
64
+ /**
65
+ * Get project mapping for a directory
66
+ * Walks up the directory tree to find the closest mapping
67
+ */
68
+ async function getProjectMapping(directoryPath) {
69
+ let config = await loadGlobalConfig();
70
+ if (!config.projects) {
71
+ return null;
72
+ }
73
+
74
+ // Walk up the directory tree looking for a mapping
75
+ let currentPath = directoryPath;
76
+ let { root } = parse(currentPath);
77
+
78
+ while (currentPath !== root) {
79
+ if (config.projects[currentPath]) {
80
+ return config.projects[currentPath];
81
+ }
82
+
83
+ // Move to parent directory
84
+ let parentPath = dirname(currentPath);
85
+ if (parentPath === currentPath) {
86
+ // We've reached the root
87
+ break;
88
+ }
89
+ currentPath = parentPath;
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * Resolve token with priority system
97
+ * Priority order:
98
+ * 1. Explicitly provided token parameter
99
+ * 2. Environment variable (VIZZLY_TOKEN)
100
+ * 3. Project mapping for working directory
101
+ * 4. User access token from global config
102
+ *
103
+ * @param {Object} options - Resolution options
104
+ * @param {string} options.providedToken - Explicitly provided token (highest priority)
105
+ * @param {string} options.workingDirectory - Working directory for project mapping lookup
106
+ * @returns {Promise<string|null>} Resolved token or null
107
+ */
108
+ export async function resolveToken(options = {}) {
109
+ let { providedToken, workingDirectory } = options;
110
+
111
+ // Priority 1: Explicitly provided token
112
+ if (providedToken) {
113
+ return providedToken;
114
+ }
115
+
116
+ // Priority 2: Environment variable
117
+ if (process.env.VIZZLY_TOKEN) {
118
+ return process.env.VIZZLY_TOKEN;
119
+ }
120
+
121
+ // Priority 3: Project mapping (if working directory provided)
122
+ if (workingDirectory) {
123
+ try {
124
+ let projectMapping = await getProjectMapping(workingDirectory);
125
+ if (projectMapping && projectMapping.token) {
126
+ // Handle both string tokens and token objects
127
+ let token = typeof projectMapping.token === 'string'
128
+ ? projectMapping.token
129
+ : projectMapping.token.token;
130
+ if (token) {
131
+ return token;
132
+ }
133
+ }
134
+ } catch (error) {
135
+ console.error('Warning: Failed to load project mapping:', error.message);
136
+ }
137
+ }
138
+
139
+ // Priority 4: User access token
140
+ try {
141
+ let accessToken = await getAccessToken();
142
+ if (accessToken) {
143
+ return accessToken;
144
+ }
145
+ } catch (error) {
146
+ console.error('Warning: Failed to load user access token:', error.message);
147
+ }
148
+
149
+ return null;
150
+ }
151
+
152
+ /**
153
+ * Check if the user has valid authentication
154
+ * @returns {Promise<boolean>} True if user has valid, non-expired authentication
155
+ */
156
+ export async function hasValidAuth() {
157
+ let auth = await getAuthTokens();
158
+
159
+ if (!auth || !auth.accessToken) {
160
+ return false;
161
+ }
162
+
163
+ // Check if token is expired
164
+ if (auth.expiresAt) {
165
+ let expiresAt = new Date(auth.expiresAt);
166
+ let now = new Date();
167
+
168
+ // Consider expired if within 5 minutes of expiry
169
+ let bufferMs = 5 * 60 * 1000;
170
+ if (now.getTime() >= expiresAt.getTime() - bufferMs) {
171
+ return false;
172
+ }
173
+ }
174
+
175
+ return true;
176
+ }
177
+
178
+ /**
179
+ * Get user information from global config
180
+ * @returns {Promise<Object|null>} User object or null if not authenticated
181
+ */
182
+ export async function getUserInfo() {
183
+ let auth = await getAuthTokens();
184
+ return auth?.user || null;
185
+ }
package/dist/cli.js CHANGED
@@ -9,6 +9,10 @@ import { tddStartCommand, tddStopCommand, tddStatusCommand, runDaemonChild } fro
9
9
  import { statusCommand, validateStatusOptions } from './commands/status.js';
10
10
  import { finalizeCommand, validateFinalizeOptions } from './commands/finalize.js';
11
11
  import { doctorCommand, validateDoctorOptions } from './commands/doctor.js';
12
+ import { loginCommand, validateLoginOptions } from './commands/login.js';
13
+ import { logoutCommand, validateLogoutOptions } from './commands/logout.js';
14
+ import { whoamiCommand, validateWhoamiOptions } from './commands/whoami.js';
15
+ import { projectSelectCommand, projectListCommand, projectTokenCommand, projectRemoveCommand, validateProjectOptions } from './commands/project.js';
12
16
  import { getPackageVersion } from './utils/package-info.js';
13
17
  import { loadPlugins } from './plugin-loader.js';
14
18
  import { loadConfig } from './utils/config-loader.js';
@@ -194,4 +198,64 @@ program.command('doctor').description('Run diagnostics to check your environment
194
198
  }
195
199
  await doctorCommand(options, globalOptions);
196
200
  });
201
+ program.command('login').description('Authenticate with your Vizzly account').option('--api-url <url>', 'API URL override').action(async options => {
202
+ const globalOptions = program.opts();
203
+
204
+ // Validate options
205
+ const validationErrors = validateLoginOptions(options);
206
+ if (validationErrors.length > 0) {
207
+ console.error('Validation errors:');
208
+ validationErrors.forEach(error => console.error(` - ${error}`));
209
+ process.exit(1);
210
+ }
211
+ await loginCommand(options, globalOptions);
212
+ });
213
+ program.command('logout').description('Clear stored authentication tokens').option('--api-url <url>', 'API URL override').action(async options => {
214
+ const globalOptions = program.opts();
215
+
216
+ // Validate options
217
+ const validationErrors = validateLogoutOptions(options);
218
+ if (validationErrors.length > 0) {
219
+ console.error('Validation errors:');
220
+ validationErrors.forEach(error => console.error(` - ${error}`));
221
+ process.exit(1);
222
+ }
223
+ await logoutCommand(options, globalOptions);
224
+ });
225
+ program.command('whoami').description('Show current authentication status and user information').option('--api-url <url>', 'API URL override').action(async options => {
226
+ const globalOptions = program.opts();
227
+
228
+ // Validate options
229
+ const validationErrors = validateWhoamiOptions(options);
230
+ if (validationErrors.length > 0) {
231
+ console.error('Validation errors:');
232
+ validationErrors.forEach(error => console.error(` - ${error}`));
233
+ process.exit(1);
234
+ }
235
+ await whoamiCommand(options, globalOptions);
236
+ });
237
+ program.command('project:select').description('Configure project for current directory').option('--api-url <url>', 'API URL override').action(async options => {
238
+ const globalOptions = program.opts();
239
+
240
+ // Validate options
241
+ const validationErrors = validateProjectOptions(options);
242
+ if (validationErrors.length > 0) {
243
+ console.error('Validation errors:');
244
+ validationErrors.forEach(error => console.error(` - ${error}`));
245
+ process.exit(1);
246
+ }
247
+ await projectSelectCommand(options, globalOptions);
248
+ });
249
+ program.command('project:list').description('Show all configured projects').action(async options => {
250
+ const globalOptions = program.opts();
251
+ await projectListCommand(options, globalOptions);
252
+ });
253
+ program.command('project:token').description('Show project token for current directory').action(async options => {
254
+ const globalOptions = program.opts();
255
+ await projectTokenCommand(options, globalOptions);
256
+ });
257
+ program.command('project:remove').description('Remove project configuration for current directory').action(async options => {
258
+ const globalOptions = program.opts();
259
+ await projectRemoveCommand(options, globalOptions);
260
+ });
197
261
  program.parse();