@vizzly-testing/cli 0.29.6 → 0.31.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 (87) hide show
  1. package/README.md +34 -6
  2. package/dist/api/client.js +6 -3
  3. package/dist/api/endpoints.js +59 -0
  4. package/dist/api/index.js +1 -1
  5. package/dist/auth/core.js +1 -1
  6. package/dist/auth/index.js +2 -1
  7. package/dist/cli.js +206 -51
  8. package/dist/client/index.js +21 -29
  9. package/dist/commands/api.js +145 -90
  10. package/dist/commands/baselines.js +78 -47
  11. package/dist/commands/builds.js +9 -8
  12. package/dist/commands/comparisons.js +7 -8
  13. package/dist/commands/config-cmd.js +15 -7
  14. package/dist/commands/context.js +870 -0
  15. package/dist/commands/doctor.js +116 -81
  16. package/dist/commands/init.js +202 -201
  17. package/dist/commands/login.js +134 -65
  18. package/dist/commands/logout.js +18 -7
  19. package/dist/commands/preview.js +29 -57
  20. package/dist/commands/project.js +120 -0
  21. package/dist/commands/projects.js +9 -2
  22. package/dist/commands/review.js +236 -202
  23. package/dist/commands/run.js +75 -72
  24. package/dist/commands/status.js +228 -150
  25. package/dist/commands/tdd-daemon-process.js +98 -0
  26. package/dist/commands/tdd-daemon.js +318 -212
  27. package/dist/commands/tdd.js +23 -8
  28. package/dist/commands/upload.js +10 -9
  29. package/dist/commands/whoami.js +64 -26
  30. package/dist/config/core.js +37 -36
  31. package/dist/context/local-workspace-provider.js +532 -0
  32. package/dist/context/provider-resolver.js +36 -0
  33. package/dist/index.js +5 -4
  34. package/dist/plugin-loader.js +55 -49
  35. package/dist/project/core.js +2 -2
  36. package/dist/project/operations.js +6 -3
  37. package/dist/reporter/reporter-bundle.css +2 -1
  38. package/dist/reporter/reporter-bundle.iife.js +119 -119
  39. package/dist/reporter-ssr/ssr-entry.js +605 -509
  40. package/dist/sdk/index.js +162 -107
  41. package/dist/server/handlers/tdd-handler.js +30 -54
  42. package/dist/server/routers/assets.js +4 -0
  43. package/dist/server/routers/baseline.js +195 -178
  44. package/dist/server/routers/events.js +226 -148
  45. package/dist/server/routers/projects.js +14 -5
  46. package/dist/server/routers/screenshot.js +1 -31
  47. package/dist/services/build-manager.js +19 -97
  48. package/dist/services/config-service.js +26 -34
  49. package/dist/services/index.js +2 -4
  50. package/dist/services/static-report-generator.js +11 -12
  51. package/dist/services/test-runner.js +21 -27
  52. package/dist/tdd/core/hotspot-coverage.js +1 -1
  53. package/dist/tdd/index.js +1 -1
  54. package/dist/tdd/metadata/region-metadata.js +0 -37
  55. package/dist/tdd/runtime-context.js +78 -0
  56. package/dist/tdd/server-registry.js +38 -17
  57. package/dist/tdd/services/baseline-manager.js +23 -0
  58. package/dist/tdd/services/hotspot-service.js +1 -14
  59. package/dist/tdd/tdd-service.js +82 -113
  60. package/dist/types/client.d.ts +36 -1
  61. package/dist/types/index.d.ts +114 -22
  62. package/dist/types/sdk.d.ts +47 -7
  63. package/dist/uploader/core.js +5 -4
  64. package/dist/uploader/index.js +79 -2
  65. package/dist/uploader/operations.js +40 -5
  66. package/dist/utils/api-url.js +14 -0
  67. package/dist/utils/async-utils.js +26 -0
  68. package/dist/utils/ci-env.js +19 -12
  69. package/dist/utils/colors.js +46 -44
  70. package/dist/utils/config-loader.js +39 -65
  71. package/dist/utils/config-schema.js +19 -55
  72. package/dist/utils/fetch-utils.js +12 -5
  73. package/dist/utils/git.js +35 -72
  74. package/dist/utils/global-config.js +22 -3
  75. package/dist/utils/output.js +19 -7
  76. package/dist/utils/patterns.js +14 -0
  77. package/dist/utils/project-link-store.js +105 -0
  78. package/dist/utils/report-data.js +31 -0
  79. package/dist/utils/screenshot-options.js +28 -0
  80. package/dist/utils/secret-store.js +65 -0
  81. package/dist/utils/security.js +3 -2
  82. package/package.json +14 -11
  83. package/dist/services/screenshot-server.js +0 -65
  84. package/dist/services/server-manager.js +0 -81
  85. package/dist/services/uploader.js +0 -100
  86. package/dist/tdd/services/region-service.js +0 -75
  87. package/dist/utils/build-history.js +0 -103
@@ -0,0 +1,532 @@
1
+ import { existsSync as defaultExistsSync, readFileSync } from 'node:fs';
2
+ import { basename, isAbsolute, join } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { normalizeReportData } from '../utils/report-data.js';
5
+ let LOCAL_CONTEXT_SOURCE = 'local_workspace';
6
+ let DEFAULT_LOCAL_REVIEW_QUEUE_LIMIT = 50;
7
+ function readJsonIfExists(path) {
8
+ if (!defaultExistsSync(path)) {
9
+ return null;
10
+ }
11
+ try {
12
+ return JSON.parse(readFileSync(path, 'utf8'));
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ function createEmptyReportData() {
18
+ return {
19
+ timestamp: Date.now(),
20
+ comparisons: [],
21
+ summary: {
22
+ total: 0,
23
+ passed: 0,
24
+ failed: 0,
25
+ rejected: 0,
26
+ errors: 0
27
+ }
28
+ };
29
+ }
30
+ function mapComparisonResult(status) {
31
+ if (status === 'new') {
32
+ return 'new';
33
+ }
34
+ if (status === 'failed' || status === 'rejected') {
35
+ return 'changed';
36
+ }
37
+ if (status === 'passed' || status === 'baseline-created') {
38
+ return 'identical';
39
+ }
40
+ if (status === 'error') {
41
+ return 'error';
42
+ }
43
+ return status || 'unknown';
44
+ }
45
+ function mapApprovalStatus(status) {
46
+ if (status === 'failed' || status === 'new') {
47
+ return 'pending';
48
+ }
49
+ if (status === 'rejected') {
50
+ return 'rejected';
51
+ }
52
+ if (status === 'passed' || status === 'baseline-created') {
53
+ return 'approved';
54
+ }
55
+ return status || 'unknown';
56
+ }
57
+ function buildLocalScope(projectRoot) {
58
+ let projectName = basename(projectRoot);
59
+ return {
60
+ organization: {
61
+ id: null,
62
+ name: 'Local Workspace',
63
+ slug: 'local'
64
+ },
65
+ project: {
66
+ id: null,
67
+ name: projectName,
68
+ slug: projectName
69
+ }
70
+ };
71
+ }
72
+ function resolveAssetReference(assetPath, snapshot) {
73
+ if (!assetPath) {
74
+ return null;
75
+ }
76
+ if (/^https?:\/\//.test(assetPath)) {
77
+ return assetPath;
78
+ }
79
+ if (assetPath.startsWith('/images/')) {
80
+ if (snapshot.serverInfo?.port) {
81
+ return `http://127.0.0.1:${snapshot.serverInfo.port}${assetPath}`;
82
+ }
83
+ return join(snapshot.vizzlyDir, assetPath.replace('/images/', ''));
84
+ }
85
+ if (isAbsolute(assetPath)) {
86
+ return assetPath;
87
+ }
88
+ return assetPath;
89
+ }
90
+ function normalizeConfirmedRegions(regions = []) {
91
+ return regions.map((region, index) => ({
92
+ id: region.id || `local-region-${index}`,
93
+ x1: region.x1 ?? region.x ?? null,
94
+ y1: region.y1 ?? region.y ?? null,
95
+ x2: region.x2 ?? (region.x != null && region.width != null ? region.x + region.width : null),
96
+ y2: region.y2 ?? (region.y != null && region.height != null ? region.y + region.height : null),
97
+ label: region.label || null
98
+ }));
99
+ }
100
+ function mergeConfirmedRegions(snapshot, comparisonName, details = {}) {
101
+ let workspaceRegions = snapshot.regions?.[comparisonName]?.confirmed || [];
102
+ let detailRegions = details.confirmedRegions || [];
103
+ let merged = [...workspaceRegions, ...detailRegions];
104
+ let seen = new Set();
105
+ return normalizeConfirmedRegions(merged).filter(region => {
106
+ let key = `${region.label || ''}:${region.x1}:${region.y1}:${region.x2}:${region.y2}`;
107
+ if (seen.has(key)) {
108
+ return false;
109
+ }
110
+ seen.add(key);
111
+ return true;
112
+ });
113
+ }
114
+ function buildHotspotAnalysis(snapshot, comparisonName, details = {}) {
115
+ let hotspotMetadata = snapshot.hotspots?.[comparisonName] || null;
116
+ let hotspotAnalysis = details.hotspotAnalysis || null;
117
+ if (!hotspotMetadata && !hotspotAnalysis) {
118
+ return {
119
+ regions: [],
120
+ total_builds_analyzed: 0,
121
+ confidence: 'no_data',
122
+ confidence_score: null,
123
+ data_source: 'local_workspace'
124
+ };
125
+ }
126
+ return {
127
+ regions: hotspotMetadata?.regions || [],
128
+ total_builds_analyzed: 1,
129
+ confidence: hotspotAnalysis?.confidence || hotspotMetadata?.confidence || 'workspace',
130
+ confidence_score: hotspotAnalysis?.confidenceScore || null,
131
+ data_source: 'local_workspace'
132
+ };
133
+ }
134
+ function buildComparisonLinks(snapshot, comparisonId) {
135
+ if (!snapshot.serverInfo?.port) {
136
+ return {};
137
+ }
138
+ return {
139
+ build_url: `http://127.0.0.1:${snapshot.serverInfo.port}/builds`,
140
+ comparison_url: `http://127.0.0.1:${snapshot.serverInfo.port}/comparison/${encodeURIComponent(comparisonId)}`
141
+ };
142
+ }
143
+ function buildBuildSnapshot(snapshot) {
144
+ let buildId = snapshot.session?.buildId || snapshot.serverInfo?.buildId || 'local-workspace';
145
+ return {
146
+ id: buildId,
147
+ name: buildId,
148
+ branch: snapshot.session?.branch || 'local',
149
+ commit_sha: snapshot.session?.commit || null,
150
+ commit_message: null,
151
+ approval_status: snapshot.serverInfo ? 'pending' : 'approved',
152
+ status: snapshot.serverInfo ? 'running' : 'completed',
153
+ created_at: snapshot.session?.createdAt || null
154
+ };
155
+ }
156
+ function buildBaselineSnapshot(snapshot) {
157
+ let metadata = snapshot.baselineMetadata;
158
+ if (!metadata) {
159
+ return {
160
+ selected: null,
161
+ selection_reason: 'no_local_baseline_metadata',
162
+ comparison_baseline_build_ids: []
163
+ };
164
+ }
165
+ return {
166
+ selected: {
167
+ id: metadata.buildId || 'local-baseline',
168
+ name: metadata.buildName || metadata.buildId || 'Local TDD Baseline',
169
+ branch: metadata.branch || 'local',
170
+ commit_sha: metadata.buildInfo?.commitSha || null,
171
+ commit_message: metadata.buildInfo?.commitMessage || null,
172
+ approval_status: metadata.buildInfo?.approvalStatus || 'approved',
173
+ status: metadata.buildInfo?.completedAt ? 'completed' : 'local',
174
+ created_at: metadata.createdAt || null,
175
+ completed_at: metadata.buildInfo?.completedAt || null
176
+ },
177
+ selection_reason: 'local_workspace_baseline_metadata',
178
+ comparison_baseline_build_ids: metadata.buildId ? [metadata.buildId] : []
179
+ };
180
+ }
181
+ function buildReviewState(build, reviewSummary) {
182
+ let reasons = [];
183
+ if (build.approval_status === 'pending') {
184
+ reasons.push('build_pending_approval');
185
+ }
186
+ if (reviewSummary.pending > 0) {
187
+ reasons.push('comparisons_need_review');
188
+ }
189
+ return {
190
+ needs_review: reasons.length > 0,
191
+ reasons,
192
+ pending_comparisons: reviewSummary.pending,
193
+ unresolved_comments: 0
194
+ };
195
+ }
196
+ function mapLocalScreenshot(snapshot, comparison) {
197
+ let mapped = mapLocalComparison(snapshot, comparison);
198
+ let baselineBuildId = snapshot.baselineMetadata?.buildId || null;
199
+ return {
200
+ id: mapped.screenshot.id,
201
+ name: mapped.screenshot.name,
202
+ browser: mapped.screenshot.browser,
203
+ viewport: {
204
+ width: mapped.screenshot.viewport_width,
205
+ height: mapped.screenshot.viewport_height
206
+ },
207
+ url: mapped.screenshot.original_url,
208
+ baseline: mapped.baseline ? {
209
+ id: mapped.baseline.id,
210
+ build_id: baselineBuildId,
211
+ name: mapped.baseline.name,
212
+ browser: mapped.baseline.browser,
213
+ viewport: {
214
+ width: mapped.baseline.viewport_width,
215
+ height: mapped.baseline.viewport_height
216
+ },
217
+ url: mapped.baseline.original_url
218
+ } : null
219
+ };
220
+ }
221
+ function mapLocalComparison(snapshot, comparison) {
222
+ let details = snapshot.comparisonDetails[comparison.id] || {};
223
+ let comparisonName = comparison.originalName || comparison.name;
224
+ let confirmedRegions = mergeConfirmedRegions(snapshot, comparisonName, details);
225
+ let hotspotAnalysis = buildHotspotAnalysis(snapshot, comparisonName, details);
226
+ let properties = comparison.properties || {};
227
+ let buildSnapshot = buildBuildSnapshot(snapshot);
228
+ let result = mapComparisonResult(comparison.status);
229
+ let approvalStatus = mapApprovalStatus(comparison.status);
230
+ let baselineBuildId = snapshot.baselineMetadata?.buildId || null;
231
+ let diffImageUrl = resolveAssetReference(comparison.diff, snapshot);
232
+ let diffRegions = details.diffClusters || [];
233
+ return {
234
+ id: comparison.id,
235
+ name: comparisonName,
236
+ screenshot_name: comparisonName,
237
+ status: comparison.status,
238
+ result,
239
+ approval_status: approvalStatus,
240
+ needs_review: approvalStatus === 'pending' && ['changed', 'new'].includes(result),
241
+ build_id: buildSnapshot.id,
242
+ build_name: buildSnapshot.name,
243
+ build_branch: buildSnapshot.branch,
244
+ build_commit_sha: buildSnapshot.commit_sha,
245
+ build_created_at: buildSnapshot.created_at,
246
+ threshold: comparison.threshold ?? null,
247
+ diff_percentage: comparison.diffPercentage ?? null,
248
+ changed_pixels: comparison.diffCount ?? null,
249
+ total_pixels: comparison.totalPixels ?? null,
250
+ screenshot: {
251
+ id: comparison.id,
252
+ name: comparisonName,
253
+ browser: properties.browser ?? null,
254
+ viewport_width: properties.viewport_width ?? null,
255
+ viewport_height: properties.viewport_height ?? null,
256
+ original_url: resolveAssetReference(comparison.current, snapshot)
257
+ },
258
+ baseline: comparison.baseline ? {
259
+ id: `${comparison.id}-baseline`,
260
+ build_id: baselineBuildId,
261
+ name: comparisonName,
262
+ browser: properties.browser ?? null,
263
+ viewport_width: properties.viewport_width ?? null,
264
+ viewport_height: properties.viewport_height ?? null,
265
+ original_url: resolveAssetReference(comparison.baseline, snapshot)
266
+ } : null,
267
+ diff: {
268
+ percentage: comparison.diffPercentage ?? null,
269
+ changed_pixels: comparison.diffCount ?? null,
270
+ total_pixels: comparison.totalPixels ?? null,
271
+ threshold: comparison.threshold ?? null,
272
+ image_url: diffImageUrl,
273
+ regions: diffRegions,
274
+ cluster_metadata: diffRegions.length ? {
275
+ clusterCount: diffRegions.length,
276
+ local_workspace: true
277
+ } : null,
278
+ fingerprint_hash: null,
279
+ fingerprint_data: null,
280
+ diff_lines: []
281
+ },
282
+ analysis: {
283
+ diff_image_url: diffImageUrl,
284
+ diff_regions: diffRegions,
285
+ cluster_metadata: diffRegions.length ? {
286
+ clusterCount: diffRegions.length,
287
+ local_workspace: true
288
+ } : null,
289
+ diff_lines: null,
290
+ fingerprint_hash: null,
291
+ fingerprint_data: null,
292
+ hotspot_analysis: hotspotAnalysis,
293
+ region_analysis: details.regionAnalysis || null,
294
+ confirmed_regions: confirmedRegions
295
+ }
296
+ };
297
+ }
298
+ function buildReviewSummary(comparisons = []) {
299
+ let approved = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'approved').length;
300
+ let rejected = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'rejected').length;
301
+ let pending = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'pending').length;
302
+ return {
303
+ total: comparisons.length,
304
+ pending,
305
+ approved,
306
+ rejected
307
+ };
308
+ }
309
+ function createLocalWorkspaceError(message) {
310
+ let error = new Error(message);
311
+ error.code = 'LOCAL_WORKSPACE_CONTEXT';
312
+ return error;
313
+ }
314
+ function createReportUrl(snapshot) {
315
+ let reportPath = join(snapshot.vizzlyDir, 'report', 'index.html');
316
+ if (!snapshot.existsSync(reportPath)) {
317
+ return null;
318
+ }
319
+ return pathToFileURL(reportPath).href;
320
+ }
321
+ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
322
+ let projectRoot = options.projectRoot || process.cwd();
323
+ let readJson = deps.readJsonIfExists || readJsonIfExists;
324
+ let existsSync = deps.existsSync || defaultExistsSync;
325
+ let snapshotCache = null;
326
+ function loadSnapshot() {
327
+ if (snapshotCache) {
328
+ return snapshotCache;
329
+ }
330
+ let vizzlyDir = join(projectRoot, '.vizzly');
331
+ snapshotCache = {
332
+ projectRoot,
333
+ vizzlyDir,
334
+ existsSync,
335
+ serverInfo: readJson(join(vizzlyDir, 'server.json')),
336
+ session: readJson(join(vizzlyDir, 'session.json')),
337
+ reportData: normalizeReportData(readJson(join(vizzlyDir, 'report-data.json')) || createEmptyReportData()),
338
+ comparisonDetails: readJson(join(vizzlyDir, 'comparison-details.json')) || {},
339
+ baselineMetadata: readJson(join(vizzlyDir, 'baselines', 'metadata.json')),
340
+ hotspotFile: readJson(join(vizzlyDir, 'hotspots.json')),
341
+ regionFile: readJson(join(vizzlyDir, 'regions.json'))
342
+ };
343
+ snapshotCache.hotspots = snapshotCache.hotspotFile?.hotspots || null;
344
+ snapshotCache.regions = snapshotCache.regionFile?.regions || null;
345
+ return snapshotCache;
346
+ }
347
+ function isAvailable(snapshot = loadSnapshot()) {
348
+ return Boolean(snapshot.serverInfo || snapshot.session || snapshot.reportData.comparisons.length > 0 || snapshot.baselineMetadata);
349
+ }
350
+ function findComparison(snapshot, target) {
351
+ if (!target) {
352
+ return null;
353
+ }
354
+ return snapshot.reportData.comparisons.find(comparison => comparison.id === target) || snapshot.reportData.comparisons.find(comparison => comparison.signature === target) || snapshot.reportData.comparisons.find(comparison => (comparison.originalName || comparison.name) === target) || null;
355
+ }
356
+ function canHandle(command, target, snapshot = loadSnapshot()) {
357
+ if (!isAvailable(snapshot)) {
358
+ return false;
359
+ }
360
+ if (command === 'build') {
361
+ let buildId = buildBuildSnapshot(snapshot).id;
362
+ return target === 'current' || target === 'local' || target === buildId;
363
+ }
364
+ if (command === 'comparison') {
365
+ return Boolean(findComparison(snapshot, target));
366
+ }
367
+ if (command === 'screenshot') {
368
+ return Boolean(snapshot.reportData.comparisons.some(comparison => (comparison.originalName || comparison.name) === target) || snapshot.regions?.[target] || snapshot.hotspots?.[target]);
369
+ }
370
+ if (command === 'review-queue') {
371
+ return snapshot.reportData.comparisons.length > 0;
372
+ }
373
+ if (command === 'similar') {
374
+ return false;
375
+ }
376
+ return false;
377
+ }
378
+ function createScope() {
379
+ return buildLocalScope(projectRoot);
380
+ }
381
+ function createBuildLinks(snapshot) {
382
+ let reportUrl = createReportUrl(snapshot);
383
+ if (!snapshot.serverInfo?.port) {
384
+ return reportUrl ? {
385
+ report_url: reportUrl
386
+ } : {};
387
+ }
388
+ let buildId = buildBuildSnapshot(snapshot).id;
389
+ return {
390
+ build_url: `http://127.0.0.1:${snapshot.serverInfo.port}/builds`,
391
+ comparison_url_prefix: `http://127.0.0.1:${snapshot.serverInfo.port}/comparison`,
392
+ current_build_id: buildId,
393
+ ...(reportUrl ? {
394
+ report_url: reportUrl
395
+ } : {})
396
+ };
397
+ }
398
+ function getBuildContext(buildId) {
399
+ let snapshot = loadSnapshot();
400
+ let resolvedBuild = buildBuildSnapshot(snapshot);
401
+ if (!(buildId === 'current' || buildId === 'local' || buildId === resolvedBuild.id)) {
402
+ throw createLocalWorkspaceError(`Local workspace context is only available for the active session build (${resolvedBuild.id})`);
403
+ }
404
+ let mappedComparisons = snapshot.reportData.comparisons.map(comparison => mapLocalComparison(snapshot, comparison));
405
+ let mappedScreenshots = snapshot.reportData.comparisons.map(comparison => mapLocalScreenshot(snapshot, comparison));
406
+ let reviewSummary = buildReviewSummary(snapshot.reportData.comparisons);
407
+ let reviewState = buildReviewState(resolvedBuild, reviewSummary);
408
+ return {
409
+ resource: 'build_context',
410
+ source: LOCAL_CONTEXT_SOURCE,
411
+ scope: createScope(),
412
+ build: resolvedBuild,
413
+ baseline: buildBaselineSnapshot(snapshot),
414
+ status: reviewState,
415
+ summary: {
416
+ comparisons: {
417
+ total: mappedComparisons.length,
418
+ changed: mappedComparisons.filter(comparison => comparison.result === 'changed').length,
419
+ new: mappedComparisons.filter(comparison => comparison.result === 'new').length
420
+ },
421
+ review: reviewSummary,
422
+ comments: {
423
+ build: 0,
424
+ screenshot: 0
425
+ }
426
+ },
427
+ review: {
428
+ comments: [],
429
+ assignments: []
430
+ },
431
+ screenshots: mappedScreenshots,
432
+ comparisons: mappedComparisons,
433
+ comments: {
434
+ build: [],
435
+ screenshot_count: 0
436
+ },
437
+ links: createBuildLinks(snapshot)
438
+ };
439
+ }
440
+ function getComparisonContext(comparisonId) {
441
+ let snapshot = loadSnapshot();
442
+ let comparison = findComparison(snapshot, comparisonId);
443
+ if (!comparison) {
444
+ throw createLocalWorkspaceError(`No local comparison found for "${comparisonId}"`);
445
+ }
446
+ let mappedComparison = mapLocalComparison(snapshot, comparison);
447
+ let comparisonName = comparison.originalName || comparison.name;
448
+ let history = snapshot.reportData.comparisons.filter(candidate => candidate.id !== comparison.id && (candidate.originalName || candidate.name) === comparisonName).map(candidate => mapLocalComparison(snapshot, candidate));
449
+ return {
450
+ resource: 'comparison_context',
451
+ source: LOCAL_CONTEXT_SOURCE,
452
+ scope: createScope(),
453
+ build: buildBuildSnapshot(snapshot),
454
+ comparison: mappedComparison,
455
+ history: {
456
+ similar_by_fingerprint: [],
457
+ recent_by_name: history,
458
+ hotspot_analysis: buildHotspotAnalysis(snapshot, comparisonName, snapshot.comparisonDetails[comparison.id] || {}),
459
+ confirmed_regions: mergeConfirmedRegions(snapshot, comparisonName, snapshot.comparisonDetails[comparison.id] || {})
460
+ },
461
+ review: {
462
+ review_summary: {
463
+ total: 0,
464
+ completed: 0,
465
+ pending: 0,
466
+ approved: 0,
467
+ changes_requested: 0,
468
+ commented: 0,
469
+ has_changes_requested: false,
470
+ decisions: []
471
+ },
472
+ assignments: [],
473
+ build_comments: [],
474
+ screenshot_comments: []
475
+ },
476
+ links: buildComparisonLinks(snapshot, comparison.id)
477
+ };
478
+ }
479
+ function getScreenshotContext(screenshotName) {
480
+ let snapshot = loadSnapshot();
481
+ let matches = snapshot.reportData.comparisons.filter(comparison => (comparison.originalName || comparison.name) === screenshotName).map(comparison => mapLocalComparison(snapshot, comparison));
482
+ if (matches.length === 0 && !snapshot.regions?.[screenshotName] && !snapshot.hotspots?.[screenshotName]) {
483
+ throw createLocalWorkspaceError(`No local screenshot context found for "${screenshotName}"`);
484
+ }
485
+ return {
486
+ resource: 'screenshot_context',
487
+ source: LOCAL_CONTEXT_SOURCE,
488
+ scope: createScope(),
489
+ screenshot: {
490
+ name: screenshotName
491
+ },
492
+ hotspot_analysis: buildHotspotAnalysis(snapshot, screenshotName),
493
+ confirmed_regions: mergeConfirmedRegions(snapshot, screenshotName),
494
+ history: {
495
+ recent_comparisons: matches
496
+ }
497
+ };
498
+ }
499
+ function getReviewQueueContext(query = {}) {
500
+ let snapshot = loadSnapshot();
501
+ let unresolved = snapshot.reportData.comparisons.filter(comparison => comparison.status === 'failed' || comparison.status === 'new');
502
+ let offset = query.offset || 0;
503
+ let limit = query.limit || DEFAULT_LOCAL_REVIEW_QUEUE_LIMIT;
504
+ let visible = unresolved.slice(offset, offset + limit).map(comparison => mapLocalComparison(snapshot, comparison));
505
+ return {
506
+ resource: 'review_queue_context',
507
+ source: LOCAL_CONTEXT_SOURCE,
508
+ scope: createScope(),
509
+ summary: {
510
+ total: unresolved.length,
511
+ changed: unresolved.filter(comparison => comparison.status === 'failed').length,
512
+ new: unresolved.filter(comparison => comparison.status === 'new').length,
513
+ builds: unresolved.length > 0 ? 1 : 0
514
+ },
515
+ comparisons: visible
516
+ };
517
+ }
518
+ function getSimilarFingerprintContext() {
519
+ throw createLocalWorkspaceError('Local workspace context does not support fingerprint similarity yet. Use --source cloud for this query.');
520
+ }
521
+ return {
522
+ source: LOCAL_CONTEXT_SOURCE,
523
+ loadSnapshot,
524
+ isAvailable,
525
+ canHandle,
526
+ getBuildContext,
527
+ getComparisonContext,
528
+ getScreenshotContext,
529
+ getReviewQueueContext,
530
+ getSimilarFingerprintContext
531
+ };
532
+ }
@@ -0,0 +1,36 @@
1
+ import { createLocalWorkspaceContextProvider as defaultCreateLocalWorkspaceContextProvider } from './local-workspace-provider.js';
2
+ export function resolveContextSource(options = {}, deps = {}) {
3
+ let {
4
+ requestedSource = 'auto',
5
+ command,
6
+ target = null,
7
+ projectRoot = process.cwd(),
8
+ hasCloudScope = false
9
+ } = options;
10
+ let {
11
+ createLocalWorkspaceContextProvider = defaultCreateLocalWorkspaceContextProvider
12
+ } = deps;
13
+ if (requestedSource === 'cloud') {
14
+ return 'cloud';
15
+ }
16
+ let localProvider = createLocalWorkspaceContextProvider({
17
+ projectRoot
18
+ });
19
+ let localSnapshot = typeof localProvider.loadSnapshot === 'function' ? localProvider.loadSnapshot() : null;
20
+ let isLocalAvailable = localSnapshot == null ? localProvider.isAvailable() : localProvider.isAvailable(localSnapshot);
21
+ if (requestedSource === 'local') {
22
+ if (!isLocalAvailable) {
23
+ let error = new Error('No local workspace context found. Start a local TDD session or ensure .vizzly/ has report data.');
24
+ error.code = 'LOCAL_WORKSPACE_CONTEXT';
25
+ throw error;
26
+ }
27
+ return 'local';
28
+ }
29
+ if (hasCloudScope) {
30
+ return 'cloud';
31
+ }
32
+ if (isLocalAvailable && (localSnapshot == null ? localProvider.canHandle(command, target) : localProvider.canHandle(command, target, localSnapshot))) {
33
+ return 'local';
34
+ }
35
+ return 'cloud';
36
+ }
package/dist/index.js CHANGED
@@ -11,13 +11,14 @@ import 'dotenv/config';
11
11
  // Client exports for convenience
12
12
  export { configure, setEnabled, vizzlyScreenshot } from './client/index.js';
13
13
  // Errors
14
- export { UploadError } from './errors/vizzly-error.js';
14
+ export { AuthError, BuildError, ConfigError, NetworkError, ScreenshotError, TimeoutError, UploadError, ValidationError, VizzlyError } from './errors/vizzly-error.js';
15
+ export { createPluginServices } from './plugin-api.js';
15
16
  // Primary SDK export
16
- export { createVizzly } from './sdk/index.js';
17
+ export { createVizzly, VizzlySDK } from './sdk/index.js';
17
18
  export { createServices } from './services/index.js';
18
- // Core services (for advanced usage)
19
- export { createUploader } from './services/uploader.js';
20
19
  export { createTDDService } from './tdd/tdd-service.js';
20
+ // Core services (for advanced usage)
21
+ export { createUploader } from './uploader/index.js';
21
22
  // Configuration helper
22
23
  export { defineConfig } from './utils/config-helpers.js';
23
24
  // Utilities