@vizzly-testing/cli 0.31.0 → 0.33.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.
@@ -9,6 +9,8 @@
9
9
  * 2D boxes that users have manually confirmed via the cloud UI.
10
10
  */
11
11
 
12
+ let REGION_CENTER_TOLERANCE = 10;
13
+
12
14
  /**
13
15
  * Check if a diff cluster intersects with a region (2D box intersection)
14
16
  *
@@ -41,13 +43,77 @@ export function clusterIntersectsRegion(cluster, region) {
41
43
  let noOverlap = clusterX2 < region.x1 || clusterX1 > region.x2 || clusterY2 < region.y1 || clusterY1 > region.y2;
42
44
  return !noOverlap;
43
45
  }
46
+ function normalizeBoundingBox(cluster) {
47
+ let box = cluster?.boundingBox || cluster;
48
+ if (!box) {
49
+ return null;
50
+ }
51
+ if (Number.isFinite(box.x) && Number.isFinite(box.y) && Number.isFinite(box.width) && Number.isFinite(box.height)) {
52
+ return {
53
+ x1: box.x,
54
+ y1: box.y,
55
+ x2: box.x + box.width,
56
+ y2: box.y + box.height,
57
+ width: Math.abs(box.width),
58
+ height: Math.abs(box.height)
59
+ };
60
+ }
61
+ if (Number.isFinite(box.x1) && Number.isFinite(box.y1) && Number.isFinite(box.x2) && Number.isFinite(box.y2)) {
62
+ let width = Math.abs(box.x2 - box.x1);
63
+ let height = Math.abs(box.y2 - box.y1);
64
+ return {
65
+ x1: box.x1,
66
+ y1: box.y1,
67
+ x2: box.x2,
68
+ y2: box.y2,
69
+ width,
70
+ height
71
+ };
72
+ }
73
+ return null;
74
+ }
75
+ function getBoundingBoxCenter(box) {
76
+ return {
77
+ x: (box.x1 + box.x2) / 2,
78
+ y: (box.y1 + box.y2) / 2
79
+ };
80
+ }
81
+ function getRegionLabel(region) {
82
+ return region.id || region.label || null;
83
+ }
84
+ function clusterMatchesRegion(cluster, region) {
85
+ let clusterBox = normalizeBoundingBox(cluster);
86
+ let regionBox = normalizeBoundingBox(region);
87
+ if (!clusterBox || !regionBox) {
88
+ return false;
89
+ }
90
+ let clusterCenter = getBoundingBoxCenter(clusterBox);
91
+ let regionCenter = getBoundingBoxCenter(regionBox);
92
+ let distance = Math.sqrt((clusterCenter.x - regionCenter.x) ** 2 + (clusterCenter.y - regionCenter.y) ** 2);
93
+ return distance <= REGION_CENTER_TOLERANCE;
94
+ }
95
+ function estimateClusterPixels(cluster) {
96
+ if (Number.isFinite(cluster?.pixelCount)) {
97
+ return cluster.pixelCount;
98
+ }
99
+ let box = normalizeBoundingBox(cluster);
100
+ if (!box) {
101
+ return 0;
102
+ }
103
+ return box.width * box.height * 0.5;
104
+ }
44
105
 
45
106
  /**
46
- * Calculate what percentage of diff clusters fall within region boxes
107
+ * Calculate what percentage of changed pixels match confirmed regions.
108
+ *
109
+ * This mirrors cloud dynamic-region approval: clusters match confirmed regions
110
+ * by center proximity, then coverage is weighted by changed pixels. A huge
111
+ * unmatched change should not be hidden just because several tiny clusters
112
+ * match confirmed regions.
47
113
  *
48
114
  * @param {Array} diffClusters - Array of diff clusters from honeydiff
49
115
  * @param {Array} regions - Array of confirmed regions { x1, y1, x2, y2 }
50
- * @returns {{ coverage: number, clustersInRegions: number, totalClusters: number, matchedRegions: string[] }}
116
+ * @returns {{ coverage: number, clustersInRegions: number, totalClusters: number, matchedRegions: string[], pixelsInRegions: number, totalPixels: number }}
51
117
  */
52
118
  export function calculateRegionCoverage(diffClusters, regions) {
53
119
  if (!diffClusters || diffClusters.length === 0) {
@@ -55,7 +121,9 @@ export function calculateRegionCoverage(diffClusters, regions) {
55
121
  coverage: 0,
56
122
  clustersInRegions: 0,
57
123
  totalClusters: 0,
58
- matchedRegions: []
124
+ matchedRegions: [],
125
+ pixelsInRegions: 0,
126
+ totalPixels: 0
59
127
  };
60
128
  }
61
129
  if (!regions || regions.length === 0) {
@@ -63,35 +131,41 @@ export function calculateRegionCoverage(diffClusters, regions) {
63
131
  coverage: 0,
64
132
  clustersInRegions: 0,
65
133
  totalClusters: diffClusters.length,
66
- matchedRegions: []
134
+ matchedRegions: [],
135
+ pixelsInRegions: 0,
136
+ totalPixels: 0
67
137
  };
68
138
  }
69
139
  let clustersInRegions = 0;
70
140
  let matchedRegionIds = new Set();
141
+ let pixelsInRegions = 0;
142
+ let totalPixels = 0;
71
143
  for (let cluster of diffClusters) {
72
- // Check if this cluster intersects any region
73
- let intersectsAnyRegion = false;
144
+ let pixelCount = estimateClusterPixels(cluster);
145
+ totalPixels += pixelCount;
146
+ let matchedAnyRegion = false;
74
147
  for (let region of regions) {
75
- if (clusterIntersectsRegion(cluster, region)) {
76
- intersectsAnyRegion = true;
77
- // Track which regions were matched (for debugging/display)
78
- if (region.id) {
79
- matchedRegionIds.add(region.id);
80
- } else if (region.label) {
81
- matchedRegionIds.add(region.label);
148
+ if (clusterMatchesRegion(cluster, region)) {
149
+ matchedAnyRegion = true;
150
+ let regionLabel = getRegionLabel(region);
151
+ if (regionLabel) {
152
+ matchedRegionIds.add(regionLabel);
82
153
  }
83
154
  }
84
155
  }
85
- if (intersectsAnyRegion) {
156
+ if (matchedAnyRegion) {
86
157
  clustersInRegions++;
158
+ pixelsInRegions += pixelCount;
87
159
  }
88
160
  }
89
- let coverage = clustersInRegions / diffClusters.length;
161
+ let coverage = totalPixels > 0 ? pixelsInRegions / totalPixels : 0;
90
162
  return {
91
163
  coverage,
92
164
  clustersInRegions,
93
165
  totalClusters: diffClusters.length,
94
- matchedRegions: [...matchedRegionIds]
166
+ matchedRegions: [...matchedRegionIds],
167
+ pixelsInRegions,
168
+ totalPixels
95
169
  };
96
170
  }
97
171
 
@@ -99,17 +173,31 @@ export function calculateRegionCoverage(diffClusters, regions) {
99
173
  * Determine if a comparison should auto-pass based on region coverage
100
174
  *
101
175
  * Unlike hotspots which require confidence scoring, user-defined regions
102
- * are already confirmed by humans, so we only need the 80% threshold.
176
+ * are already confirmed by humans. Cloud still requires strong structural
177
+ * similarity so a layout shift cannot hide inside a confirmed box.
103
178
  *
104
179
  * @param {Array} regions - Confirmed regions (already filtered to confirmed status)
105
180
  * @param {{ coverage: number }} coverageResult - Result from calculateRegionCoverage
181
+ * @param {Object} options - Approval thresholds
182
+ * @param {number|null} options.ssimScore - Honeydiff SSIM/perceptual score
183
+ * @param {number} options.coverageThreshold - Required region coverage
184
+ * @param {number} options.ssimThreshold - Required structural similarity
106
185
  * @returns {boolean} True if diff should auto-pass as region-filtered
107
186
  */
108
- export function shouldAutoApproveFromRegions(regions, coverageResult) {
187
+ export function shouldAutoApproveFromRegions(regions, coverageResult, options = {}) {
109
188
  if (!regions || regions.length === 0 || !coverageResult) {
110
189
  return false;
111
190
  }
112
-
113
- // Need at least 80% of diff clusters in confirmed regions
114
- return coverageResult.coverage >= 0.8;
191
+ let {
192
+ ssimScore = null,
193
+ coverageThreshold = 0.9,
194
+ ssimThreshold = 0.95
195
+ } = options;
196
+ if (coverageResult.coverage < coverageThreshold) {
197
+ return false;
198
+ }
199
+ if (ssimScore === null || ssimScore === undefined) {
200
+ return false;
201
+ }
202
+ return ssimScore >= ssimThreshold;
115
203
  }
@@ -32,6 +32,9 @@ export async function compareImages(baselinePath, currentPath, diffPath, options
32
32
  diffPath,
33
33
  overwrite: true,
34
34
  includeClusters: true,
35
+ includeSSIM: true,
36
+ includeGMSD: true,
37
+ clusterMerge: true,
35
38
  minClusterSize
36
39
  });
37
40
  }
@@ -141,6 +144,7 @@ export function buildFailedComparison(params) {
141
144
  regionData
142
145
  } = params;
143
146
  let diffClusters = honeydiffResult.diffClusters || [];
147
+ let ssimScore = honeydiffResult.perceptualScore ?? honeydiffResult.ssimScore ?? honeydiffResult.ssim_score ?? null;
144
148
  let isFiltered = false;
145
149
  let filterReason = 'pixel-diff';
146
150
 
@@ -150,7 +154,9 @@ export function buildFailedComparison(params) {
150
154
  let confirmedRegions = regionData?.confirmed || [];
151
155
  if (confirmedRegions.length > 0 && diffClusters.length > 0) {
152
156
  regionCoverage = calculateRegionCoverage(diffClusters, confirmedRegions);
153
- if (shouldAutoApproveFromRegions(confirmedRegions, regionCoverage)) {
157
+ if (shouldAutoApproveFromRegions(confirmedRegions, regionCoverage, {
158
+ ssimScore
159
+ })) {
154
160
  isRegionFiltered = true;
155
161
  isFiltered = true;
156
162
  filterReason = 'region-filtered';
@@ -198,8 +204,11 @@ export function buildFailedComparison(params) {
198
204
  coverage: regionCoverage.coverage,
199
205
  clustersInRegions: regionCoverage.clustersInRegions,
200
206
  totalClusters: regionCoverage.totalClusters,
207
+ pixelsInRegions: regionCoverage.pixelsInRegions,
208
+ totalPixels: regionCoverage.totalPixels,
201
209
  matchedRegions: regionCoverage.matchedRegions,
202
210
  confirmedCount: confirmedRegions.length,
211
+ ssimScore,
203
212
  isFiltered: isRegionFiltered
204
213
  } : null,
205
214
  // Include confirmed regions for visualization in UI
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { existsSync as defaultExistsSync, mkdirSync as defaultMkdirSync, readFileSync as defaultReadFileSync, writeFileSync as defaultWriteFileSync } from 'node:fs';
12
- import { createApiClient as defaultCreateApiClient, getBatchHotspots as defaultGetBatchHotspots, getBuilds as defaultGetBuilds, getComparison as defaultGetComparison, getTddBaselines as defaultGetTddBaselines } from '../api/index.js';
12
+ import { createApiClient as defaultCreateApiClient, getBatchHotspots as defaultGetBatchHotspots, getBuilds as defaultGetBuilds, getComparison as defaultGetComparison, getComparisonContext as defaultGetComparisonContext, getTddBaselines as defaultGetTddBaselines } from '../api/index.js';
13
13
  import { NetworkError } from '../errors/vizzly-error.js';
14
14
  import { colors as defaultColors } from '../utils/colors.js';
15
15
  import { fetchWithTimeout as defaultFetchWithTimeout } from '../utils/fetch-utils.js';
@@ -58,6 +58,7 @@ export class TddService {
58
58
  getTddBaselines: defaultGetTddBaselines,
59
59
  getBuilds: defaultGetBuilds,
60
60
  getComparison: defaultGetComparison,
61
+ getComparisonContext: defaultGetComparisonContext,
61
62
  getBatchHotspots: defaultGetBatchHotspots,
62
63
  fetchWithTimeout: defaultFetchWithTimeout,
63
64
  getDefaultBranch: defaultGetDefaultBranch
@@ -169,6 +170,7 @@ export class TddService {
169
170
  getTddBaselines,
170
171
  getBuilds,
171
172
  getComparison,
173
+ getComparisonContext,
172
174
  clearBaselineData,
173
175
  generateScreenshotSignature,
174
176
  generateBaselineFilename,
@@ -221,10 +223,19 @@ export class TddService {
221
223
  output.warn(`Build ${buildId} has status: ${baselineBuild.status} (expected: completed)`);
222
224
  }
223
225
  baselineBuild.screenshots = apiResponse.screenshots;
226
+ this.saveDynamicMetadataFromBaselineResponse(apiResponse);
224
227
  } else if (comparisonId) {
225
228
  // Handle specific comparison download
226
229
  output.info(`Using comparison: ${comparisonId}`);
227
230
  let comparison = await getComparison(this.client, comparisonId);
231
+ let comparisonContext = null;
232
+ if (!comparison.dynamic_content && getComparisonContext) {
233
+ try {
234
+ comparisonContext = await getComparisonContext(this.client, comparisonId);
235
+ } catch (error) {
236
+ output.debug('tdd', `comparison context unavailable for ${comparisonId}: ${error.message}`);
237
+ }
238
+ }
228
239
  if (!comparison.baseline_screenshot) {
229
240
  throw new Error(`Comparison ${comparisonId} has no baseline screenshot. This comparison may be a "new" screenshot.`);
230
241
  }
@@ -255,6 +266,11 @@ export class TddService {
255
266
  }
256
267
  }
257
268
  let screenshotName = comparison.baseline_name || comparison.current_name;
269
+ let dynamicContent = comparison.dynamic_content || {
270
+ hotspot_analysis: comparisonContext?.history?.hotspot_analysis,
271
+ confirmed_regions: comparisonContext?.history?.confirmed_regions
272
+ };
273
+ this.saveDynamicMetadataForScreenshot(screenshotName, dynamicContent);
258
274
  let signature = generateScreenshotSignature(screenshotName, screenshotProperties, this.signatureProperties);
259
275
  let filename = generateBaselineFilename(screenshotName, signature);
260
276
  baselineBuild = {
@@ -294,24 +310,15 @@ export class TddService {
294
310
  }
295
311
  baselineBuild = apiResponse.build;
296
312
  baselineBuild.screenshots = apiResponse.screenshots;
297
-
298
- // Store bundled hotspots and regions from API response
299
- if (apiResponse.hotspots && Object.keys(apiResponse.hotspots).length > 0) {
300
- this.hotspotData = apiResponse.hotspots;
301
- saveHotspotMetadata(this.workingDir, apiResponse.hotspots, apiResponse.summary);
302
- }
303
- if (apiResponse.regions && Object.keys(apiResponse.regions).length > 0) {
304
- this.regionData = apiResponse.regions;
305
- saveRegionMetadata(this.workingDir, apiResponse.regions, apiResponse.summary);
306
- }
313
+ this.saveDynamicMetadataFromBaselineResponse(apiResponse);
307
314
  }
308
315
  let buildDetails = baselineBuild;
309
316
  if (!buildDetails.screenshots || buildDetails.screenshots.length === 0) {
310
317
  output.warn('No screenshots found in baseline build');
311
318
  return null;
312
319
  }
313
- output.info(`Using baseline from build: ${colors.cyan(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
314
- output.info(`Checking ${colors.cyan(buildDetails.screenshots.length)} baseline screenshots...`);
320
+ output.info(`Using baseline from build: ${colors.info(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
321
+ output.info(`Checking ${colors.info(buildDetails.screenshots.length)} baseline screenshots...`);
315
322
 
316
323
  // Check existing baseline metadata for SHA comparison
317
324
  let existingBaseline = await this.loadBaseline();
@@ -488,6 +495,49 @@ export class TddService {
488
495
  throw error;
489
496
  }
490
497
  }
498
+ saveDynamicMetadataFromBaselineResponse(apiResponse = {}) {
499
+ let {
500
+ saveHotspotMetadata,
501
+ saveRegionMetadata
502
+ } = this._deps;
503
+ let hasHotspots = Object.keys(apiResponse.hotspots || {}).length > 0;
504
+ let hasRegions = Object.keys(apiResponse.regions || {}).length > 0;
505
+ if (hasHotspots) {
506
+ this.hotspotData = apiResponse.hotspots;
507
+ saveHotspotMetadata(this.workingDir, apiResponse.hotspots, apiResponse.summary);
508
+ }
509
+ if (hasRegions) {
510
+ this.regionData = apiResponse.regions;
511
+ saveRegionMetadata(this.workingDir, apiResponse.regions, apiResponse.summary);
512
+ }
513
+ }
514
+ saveDynamicMetadataForScreenshot(screenshotName, dynamicContent = {}) {
515
+ if (!screenshotName) {
516
+ return;
517
+ }
518
+ let hotspotAnalysis = dynamicContent.hotspot_analysis;
519
+ let confirmedRegions = dynamicContent.confirmed_regions || [];
520
+ let hasHotspotAnalysis = hotspotAnalysis?.total_builds_analyzed > 0;
521
+ let hasConfirmedRegions = confirmedRegions.length > 0;
522
+ let summary = {
523
+ screenshots: 1,
524
+ hotspots: hasHotspotAnalysis ? 1 : 0,
525
+ regions: hasConfirmedRegions ? 1 : 0,
526
+ total_regions: confirmedRegions.length
527
+ };
528
+ this.saveDynamicMetadataFromBaselineResponse({
529
+ hotspots: hasHotspotAnalysis ? {
530
+ [screenshotName]: hotspotAnalysis
531
+ } : {},
532
+ regions: hasConfirmedRegions ? {
533
+ [screenshotName]: {
534
+ confirmed: confirmedRegions,
535
+ candidates: []
536
+ }
537
+ } : {},
538
+ summary
539
+ });
540
+ }
491
541
 
492
542
  /**
493
543
  * Process already-fetched baseline data (for use when caller handles auth)
@@ -526,20 +576,7 @@ export class TddService {
526
576
  }
527
577
  }
528
578
  let baselineBuild = apiResponse.build;
529
-
530
- // Store bundled hotspots and regions from API response
531
- let {
532
- saveHotspotMetadata,
533
- saveRegionMetadata
534
- } = this._deps;
535
- if (apiResponse.hotspots && Object.keys(apiResponse.hotspots).length > 0) {
536
- this.hotspotData = apiResponse.hotspots;
537
- saveHotspotMetadata(this.workingDir, apiResponse.hotspots, apiResponse.summary);
538
- }
539
- if (apiResponse.regions && Object.keys(apiResponse.regions).length > 0) {
540
- this.regionData = apiResponse.regions;
541
- saveRegionMetadata(this.workingDir, apiResponse.regions, apiResponse.summary);
542
- }
579
+ this.saveDynamicMetadataFromBaselineResponse(apiResponse);
543
580
  if (baselineBuild.status === 'failed') {
544
581
  output.warn(`Build ${buildId} is marked as FAILED - falling back to local baselines`);
545
582
  return await this.handleLocalBaselines();
@@ -552,8 +589,8 @@ export class TddService {
552
589
  output.warn('No screenshots found in baseline build');
553
590
  return null;
554
591
  }
555
- output.info(`Using baseline from build: ${colors.cyan(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
556
- output.info(`Checking ${colors.cyan(buildDetails.screenshots.length)} baseline screenshots...`);
592
+ output.info(`Using baseline from build: ${colors.info(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
593
+ output.info(`Checking ${colors.info(buildDetails.screenshots.length)} baseline screenshots...`);
557
594
 
558
595
  // Check existing baseline metadata for SHA comparison
559
596
  let existingBaseline = await this.loadBaseline();
@@ -862,7 +899,7 @@ export class TddService {
862
899
  }
863
900
  return null;
864
901
  } else {
865
- output.info(`Using existing baseline: ${colors.cyan(baseline.buildName)}`);
902
+ output.info(`Using existing baseline: ${colors.info(baseline.buildName)}`);
866
903
  return baseline;
867
904
  }
868
905
  }
@@ -1132,9 +1169,9 @@ export class TddService {
1132
1169
  output.print(`🐻 ${colors.bold(results.total)} screenshot${results.total !== 1 ? 's' : ''} compared`);
1133
1170
  output.blank();
1134
1171
 
1135
- // Passed section - use Observatory success color
1172
+ // Passed section - use BearDen success color
1136
1173
  if (results.passed > 0) {
1137
- let successColor = colors.brand?.success || colors.green;
1174
+ let successColor = colors.brand?.success || colors.success;
1138
1175
  if (output.isVerbose()) {
1139
1176
  // Verbose mode: show each screenshot
1140
1177
  for (let comp of passedComparisons) {
@@ -1147,10 +1184,10 @@ export class TddService {
1147
1184
  output.blank();
1148
1185
  }
1149
1186
 
1150
- // Failed comparisons with diff bars - use Observatory warning/danger colors
1187
+ // Failed comparisons with diff bars - use BearDen warning/danger colors
1151
1188
  if (failedComparisons.length > 0) {
1152
1189
  let warningColor = colors.brand?.warning || colors.yellow;
1153
- let dangerColor = colors.brand?.danger || colors.red;
1190
+ let dangerColor = colors.brand?.danger || colors.error;
1154
1191
  output.print(` ${warningColor('◐')} ${warningColor(failedComparisons.length)} visual change${failedComparisons.length !== 1 ? 's' : ''} detected`);
1155
1192
 
1156
1193
  // Find longest name for alignment
@@ -1171,9 +1208,9 @@ export class TddService {
1171
1208
  output.blank();
1172
1209
  }
1173
1210
 
1174
- // New screenshots - use Observatory info color
1211
+ // New screenshots - use BearDen info color
1175
1212
  if (newComparisons.length > 0) {
1176
- let infoColor = colors.brand?.info || colors.cyan;
1213
+ let infoColor = colors.brand?.info || colors.info;
1177
1214
  let textMuted = colors.brand?.textMuted || colors.dim;
1178
1215
  output.print(` ${infoColor('+')} ${infoColor(newComparisons.length)} new screenshot${newComparisons.length !== 1 ? 's' : ''}`);
1179
1216
  for (let comp of newComparisons) {
@@ -1182,9 +1219,9 @@ export class TddService {
1182
1219
  output.blank();
1183
1220
  }
1184
1221
 
1185
- // Errors - use Observatory danger color
1222
+ // Errors - use BearDen danger color
1186
1223
  if (results.errors > 0) {
1187
- let dangerColor = colors.brand?.danger || colors.red;
1224
+ let dangerColor = colors.brand?.danger || colors.error;
1188
1225
  let errorComparisons = this.comparisons.filter(c => c.status === 'error');
1189
1226
  output.print(` ${dangerColor('!')} ${dangerColor(results.errors)} error${results.errors !== 1 ? 's' : ''}`);
1190
1227
  for (let comp of errorComparisons) {
@@ -1195,7 +1232,7 @@ export class TddService {
1195
1232
 
1196
1233
  // Dashboard link with prominent styling - detect if server is running
1197
1234
  if (hasChanges) {
1198
- let infoColor = colors.brand?.info || colors.cyan;
1235
+ let infoColor = colors.brand?.info || colors.info;
1199
1236
  let textTertiary = colors.brand?.textTertiary || colors.dim;
1200
1237
 
1201
1238
  // Check if TDD server is already running
@@ -4,56 +4,47 @@
4
4
  import { Ansis } from 'ansis';
5
5
 
6
6
  // =============================================================================
7
- // Vizzly Observatory Design System Colors
8
- // Aligned with @vizzly-testing/observatory color tokens
7
+ // Vizzly BearDen Design System Colors
8
+ // Aligned with the reporter design system color tokens.
9
9
  // =============================================================================
10
10
 
11
11
  export let brand = {
12
- // Primary brand color - Amber is Observatory's signature
13
- amber: '#F59E0B',
12
+ // Primary brand color - restrained brass on graphite
13
+ amber: '#D69A35',
14
14
  // Primary brand, actions, highlights
15
- amberLight: '#FBBF24',
15
+ amberLight: '#E0AD55',
16
16
  // Hover states, emphasis
17
17
 
18
18
  // Accent colors (semantic)
19
- success: '#10B981',
19
+ success: '#7FD990',
20
20
  // Approved, passed, active (--accent-success)
21
- warning: '#F59E0B',
21
+ warning: '#DCAD5F',
22
22
  // Pending, attention (--accent-warning)
23
- danger: '#EF4444',
23
+ danger: '#D77782',
24
24
  // Rejected, failed, errors (--accent-danger)
25
- info: '#3B82F6',
25
+ info: '#B7BDC6',
26
26
  // Processing, informational (--accent-info)
27
27
 
28
28
  // Surface colors (dark theme)
29
- bg: '#0F172A',
29
+ bg: '#07080A',
30
30
  // Page background (--vz-bg)
31
- surface: '#1A2332',
31
+ surface: '#101216',
32
32
  // Cards, panels (--vz-surface)
33
- elevated: '#1E293B',
33
+ elevated: '#111318',
34
34
  // Dropdowns, modals (--vz-elevated)
35
- border: '#374151',
35
+ border: '#30343B',
36
36
  // Primary borders (--vz-border)
37
- borderSubtle: '#2D3748',
37
+ borderSubtle: '#252930',
38
38
  // Subtle dividers (--vz-border-subtle)
39
39
 
40
40
  // Text hierarchy
41
- textPrimary: '#FFFFFF',
41
+ textPrimary: '#F4F6F8',
42
42
  // Headings, important (--text-primary)
43
- textSecondary: '#9CA3AF',
43
+ textSecondary: '#C8CDD3',
44
44
  // Body text (--text-secondary)
45
- textTertiary: '#6B7280',
45
+ textTertiary: '#A0A6AF',
46
46
  // Captions, metadata (--text-tertiary)
47
- textMuted: '#4B5563',
48
- // Disabled, placeholders (--text-muted)
49
-
50
- // Legacy aliases (for backward compatibility)
51
- green: '#10B981',
52
- red: '#EF4444',
53
- cyan: '#06B6D4',
54
- // Still useful for links in terminals
55
- slate: '#64748B',
56
- dark: '#1E293B'
47
+ textMuted: '#7A828D' // Disabled, placeholders (--text-muted)
57
48
  };
58
49
  function supportsColorDefault() {
59
50
  // Respect NO_COLOR: https://no-color.org/
@@ -124,7 +115,7 @@ export function createColors(options = {}) {
124
115
  hex: () => noop,
125
116
  bgRgb: () => noop,
126
117
  bgHex: () => noop,
127
- // Observatory brand colors (noop versions)
118
+ // BearDen brand colors (noop versions)
128
119
  brand: {
129
120
  // Primary
130
121
  amber: noop,
@@ -133,6 +124,7 @@ export function createColors(options = {}) {
133
124
  success: noop,
134
125
  warning: noop,
135
126
  danger: noop,
127
+ error: noop,
136
128
  info: noop,
137
129
  // Text hierarchy
138
130
  textPrimary: noop,
@@ -144,12 +136,7 @@ export function createColors(options = {}) {
144
136
  bgSuccess: noop,
145
137
  bgWarning: noop,
146
138
  bgDanger: noop,
147
- bgInfo: noop,
148
- // Legacy aliases
149
- green: noop,
150
- red: noop,
151
- cyan: noop,
152
- slate: noop
139
+ bgInfo: noop
153
140
  }
154
141
  };
155
142
  }
@@ -173,16 +160,16 @@ export function createColors(options = {}) {
173
160
  gray: colors.gray,
174
161
  black: colors.black,
175
162
  // Semantic aliases (basic)
176
- success: colors.green,
177
- error: colors.red,
178
- warning: colors.yellow,
179
- info: colors.blue,
163
+ success: colors.hex(brand.success),
164
+ error: colors.hex(brand.danger),
165
+ warning: colors.hex(brand.warning),
166
+ info: colors.hex(brand.info),
180
167
  // Extended colors for rich styling
181
168
  rgb: colors.rgb.bind(colors),
182
169
  hex: colors.hex.bind(colors),
183
170
  bgRgb: colors.bgRgb.bind(colors),
184
171
  bgHex: colors.bgHex.bind(colors),
185
- // Observatory brand colors (Truecolor) - aligned with design system
172
+ // BearDen brand colors (Truecolor) - aligned with design system
186
173
  brand: {
187
174
  // Primary brand color
188
175
  amber: colors.hex(brand.amber),
@@ -191,6 +178,7 @@ export function createColors(options = {}) {
191
178
  success: colors.hex(brand.success),
192
179
  warning: colors.hex(brand.warning),
193
180
  danger: colors.hex(brand.danger),
181
+ error: colors.hex(brand.danger),
194
182
  info: colors.hex(brand.info),
195
183
  // Text hierarchy
196
184
  textPrimary: colors.hex(brand.textPrimary),
@@ -202,12 +190,7 @@ export function createColors(options = {}) {
202
190
  bgSuccess: colors.bgHex(brand.success),
203
191
  bgWarning: colors.bgHex(brand.warning),
204
192
  bgDanger: colors.bgHex(brand.danger),
205
- bgInfo: colors.bgHex(brand.info),
206
- // Legacy aliases (backward compatibility)
207
- green: colors.hex(brand.green),
208
- red: colors.hex(brand.red),
209
- cyan: colors.hex(brand.cyan),
210
- slate: colors.hex(brand.slate)
193
+ bgInfo: colors.bgHex(brand.info)
211
194
  }
212
195
  };
213
196
  }
@@ -121,7 +121,7 @@ export async function clearGlobalConfig() {
121
121
 
122
122
  /**
123
123
  * Save user's PATH for menubar app to use
124
- * This auto-configures the menubar app so it can find npx/node
124
+ * This auto-configures the menubar app so it can find package runners/node
125
125
  * @returns {Promise<void>}
126
126
  */
127
127
  export async function saveUserPath() {