@vizzly-testing/cli 0.20.1-beta.0 → 0.20.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 (38) hide show
  1. package/README.md +16 -18
  2. package/dist/cli.js +177 -2
  3. package/dist/client/index.js +144 -77
  4. package/dist/commands/doctor.js +118 -33
  5. package/dist/commands/finalize.js +8 -3
  6. package/dist/commands/init.js +13 -18
  7. package/dist/commands/login.js +42 -49
  8. package/dist/commands/logout.js +13 -5
  9. package/dist/commands/project.js +95 -67
  10. package/dist/commands/run.js +32 -6
  11. package/dist/commands/status.js +81 -50
  12. package/dist/commands/tdd-daemon.js +61 -32
  13. package/dist/commands/tdd.js +14 -26
  14. package/dist/commands/upload.js +18 -9
  15. package/dist/commands/whoami.js +40 -38
  16. package/dist/reporter/reporter-bundle.css +1 -1
  17. package/dist/reporter/reporter-bundle.iife.js +204 -22
  18. package/dist/server/handlers/tdd-handler.js +113 -7
  19. package/dist/server/http-server.js +9 -3
  20. package/dist/server/routers/baseline.js +58 -0
  21. package/dist/server/routers/dashboard.js +10 -6
  22. package/dist/server/routers/screenshot.js +32 -0
  23. package/dist/server-manager/core.js +5 -2
  24. package/dist/server-manager/operations.js +2 -1
  25. package/dist/services/config-service.js +306 -0
  26. package/dist/tdd/tdd-service.js +190 -126
  27. package/dist/types/client.d.ts +25 -2
  28. package/dist/utils/colors.js +187 -39
  29. package/dist/utils/config-loader.js +3 -6
  30. package/dist/utils/context.js +228 -0
  31. package/dist/utils/output.js +449 -14
  32. package/docs/api-reference.md +173 -8
  33. package/docs/tui-elements.md +560 -0
  34. package/package.json +13 -7
  35. package/dist/report-generator/core.js +0 -315
  36. package/dist/report-generator/index.js +0 -8
  37. package/dist/report-generator/operations.js +0 -196
  38. package/dist/services/static-report-generator.js +0 -65
@@ -1,66 +1,214 @@
1
- // Zero-dependency color helper using raw ANSI codes.
2
- // Detects terminal color support and emits codes only when enabled.
1
+ // Color utility using ansis for rich terminal styling.
2
+ // Detects terminal color support and provides chainable color functions.
3
3
 
4
+ import ansis from 'ansis';
5
+
6
+ // =============================================================================
7
+ // Vizzly Observatory Design System Colors
8
+ // Aligned with @vizzly-testing/observatory color tokens
9
+ // =============================================================================
10
+
11
+ export let brand = {
12
+ // Primary brand color - Amber is Observatory's signature
13
+ amber: '#F59E0B',
14
+ // Primary brand, actions, highlights
15
+ amberLight: '#FBBF24',
16
+ // Hover states, emphasis
17
+
18
+ // Accent colors (semantic)
19
+ success: '#10B981',
20
+ // Approved, passed, active (--accent-success)
21
+ warning: '#F59E0B',
22
+ // Pending, attention (--accent-warning)
23
+ danger: '#EF4444',
24
+ // Rejected, failed, errors (--accent-danger)
25
+ info: '#3B82F6',
26
+ // Processing, informational (--accent-info)
27
+
28
+ // Surface colors (dark theme)
29
+ bg: '#0F172A',
30
+ // Page background (--vz-bg)
31
+ surface: '#1A2332',
32
+ // Cards, panels (--vz-surface)
33
+ elevated: '#1E293B',
34
+ // Dropdowns, modals (--vz-elevated)
35
+ border: '#374151',
36
+ // Primary borders (--vz-border)
37
+ borderSubtle: '#2D3748',
38
+ // Subtle dividers (--vz-border-subtle)
39
+
40
+ // Text hierarchy
41
+ textPrimary: '#FFFFFF',
42
+ // Headings, important (--text-primary)
43
+ textSecondary: '#9CA3AF',
44
+ // Body text (--text-secondary)
45
+ textTertiary: '#6B7280',
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'
57
+ };
4
58
  function supportsColorDefault() {
5
59
  // Respect NO_COLOR: https://no-color.org/
6
60
  if ('NO_COLOR' in process.env) return false;
7
61
 
8
62
  // Respect FORCE_COLOR if set to a truthy value (except '0')
9
63
  if ('FORCE_COLOR' in process.env) {
10
- const v = process.env.FORCE_COLOR;
64
+ let v = process.env.FORCE_COLOR;
11
65
  if (v && v !== '0') return true;
12
66
  if (v === '0') return false;
13
67
  }
14
68
 
69
+ // COLORTERM indicates truecolor support
70
+ if (process.env.COLORTERM === 'truecolor' || process.env.COLORTERM === '24bit') {
71
+ return true;
72
+ }
73
+
15
74
  // If stdout is not a TTY, assume no color
16
75
  if (!process.stdout || !process.stdout.isTTY) return false;
17
76
 
18
77
  // Prefer getColorDepth when available
19
78
  try {
20
- const depth = typeof process.stdout.getColorDepth === 'function' ? process.stdout.getColorDepth() : 1;
79
+ let depth = typeof process.stdout.getColorDepth === 'function' ? process.stdout.getColorDepth() : 1;
21
80
  return depth && depth > 1;
22
81
  } catch {
23
82
  // Fallback heuristic
24
83
  return true;
25
84
  }
26
85
  }
27
- function styleFn(open, close, enabled) {
28
- return (input = '') => {
29
- const str = String(input);
30
- if (!enabled) return str;
31
- return open + str + close;
32
- };
33
- }
86
+
87
+ /**
88
+ * Create a colors API with optional color support detection
89
+ * @param {Object} options - Configuration options
90
+ * @param {boolean} [options.useColor] - Force color on/off (auto-detect if undefined)
91
+ * @returns {Object} Colors API with styling functions
92
+ */
34
93
  export function createColors(options = {}) {
35
- const enabled = options.useColor !== undefined ? !!options.useColor : supportsColorDefault();
36
- const codes = {
37
- reset: ['\x1b[0m', ''],
38
- bold: ['\x1b[1m', '\x1b[22m'],
39
- dim: ['\x1b[2m', '\x1b[22m'],
40
- italic: ['\x1b[3m', '\x1b[23m'],
41
- underline: ['\x1b[4m', '\x1b[24m'],
42
- strikethrough: ['\x1b[9m', '\x1b[29m'],
43
- red: ['\x1b[31m', '\x1b[39m'],
44
- green: ['\x1b[32m', '\x1b[39m'],
45
- yellow: ['\x1b[33m', '\x1b[39m'],
46
- blue: ['\x1b[34m', '\x1b[39m'],
47
- magenta: ['\x1b[35m', '\x1b[39m'],
48
- cyan: ['\x1b[36m', '\x1b[39m'],
49
- white: ['\x1b[37m', '\x1b[39m'],
50
- gray: ['\x1b[90m', '\x1b[39m']
51
- };
52
- const api = {};
53
- for (const [name, [open, close]] of Object.entries(codes)) {
54
- api[name] = styleFn(open, close || '\x1b[0m', enabled);
94
+ let enabled = options.useColor !== undefined ? !!options.useColor : supportsColorDefault();
95
+ if (!enabled) {
96
+ // Return no-op functions when color disabled
97
+ let noop = (input = '') => String(input);
98
+ return {
99
+ // Modifiers
100
+ reset: noop,
101
+ bold: noop,
102
+ dim: noop,
103
+ italic: noop,
104
+ underline: noop,
105
+ strikethrough: noop,
106
+ // Colors
107
+ red: noop,
108
+ green: noop,
109
+ yellow: noop,
110
+ blue: noop,
111
+ magenta: noop,
112
+ cyan: noop,
113
+ white: noop,
114
+ gray: noop,
115
+ black: noop,
116
+ // Semantic aliases
117
+ success: noop,
118
+ error: noop,
119
+ warning: noop,
120
+ info: noop,
121
+ // Extended colors (return noop factory for chaining)
122
+ rgb: () => noop,
123
+ hex: () => noop,
124
+ bgRgb: () => noop,
125
+ bgHex: () => noop,
126
+ // Observatory brand colors (noop versions)
127
+ brand: {
128
+ // Primary
129
+ amber: noop,
130
+ amberLight: noop,
131
+ // Semantic accents
132
+ success: noop,
133
+ warning: noop,
134
+ danger: noop,
135
+ info: noop,
136
+ // Text hierarchy
137
+ textPrimary: noop,
138
+ textSecondary: noop,
139
+ textTertiary: noop,
140
+ textMuted: noop,
141
+ // Background variants
142
+ bgAmber: noop,
143
+ bgSuccess: noop,
144
+ bgWarning: noop,
145
+ bgDanger: noop,
146
+ bgInfo: noop,
147
+ // Legacy aliases
148
+ green: noop,
149
+ red: noop,
150
+ cyan: noop,
151
+ slate: noop
152
+ }
153
+ };
55
154
  }
56
-
57
- // Semantic aliases
58
- api.success = api.green;
59
- api.error = api.red;
60
- api.warning = api.yellow;
61
- api.info = api.blue;
62
- return api;
155
+ return {
156
+ // Modifiers
157
+ reset: ansis.reset,
158
+ bold: ansis.bold,
159
+ dim: ansis.dim,
160
+ italic: ansis.italic,
161
+ underline: ansis.underline,
162
+ strikethrough: ansis.strikethrough,
163
+ // Basic ANSI colors (fallback)
164
+ red: ansis.red,
165
+ green: ansis.green,
166
+ yellow: ansis.yellow,
167
+ blue: ansis.blue,
168
+ magenta: ansis.magenta,
169
+ cyan: ansis.cyan,
170
+ white: ansis.white,
171
+ gray: ansis.gray,
172
+ black: ansis.black,
173
+ // Semantic aliases (basic)
174
+ success: ansis.green,
175
+ error: ansis.red,
176
+ warning: ansis.yellow,
177
+ info: ansis.blue,
178
+ // Extended colors for rich styling
179
+ rgb: ansis.rgb,
180
+ hex: ansis.hex,
181
+ bgRgb: ansis.bgRgb,
182
+ bgHex: ansis.bgHex,
183
+ // Observatory brand colors (Truecolor) - aligned with design system
184
+ brand: {
185
+ // Primary brand color
186
+ amber: ansis.hex(brand.amber),
187
+ amberLight: ansis.hex(brand.amberLight),
188
+ // Semantic accents
189
+ success: ansis.hex(brand.success),
190
+ warning: ansis.hex(brand.warning),
191
+ danger: ansis.hex(brand.danger),
192
+ info: ansis.hex(brand.info),
193
+ // Text hierarchy
194
+ textPrimary: ansis.hex(brand.textPrimary),
195
+ textSecondary: ansis.hex(brand.textSecondary),
196
+ textTertiary: ansis.hex(brand.textTertiary),
197
+ textMuted: ansis.hex(brand.textMuted),
198
+ // Background variants
199
+ bgAmber: ansis.bgHex(brand.amber),
200
+ bgSuccess: ansis.bgHex(brand.success),
201
+ bgWarning: ansis.bgHex(brand.warning),
202
+ bgDanger: ansis.bgHex(brand.danger),
203
+ bgInfo: ansis.bgHex(brand.info),
204
+ // Legacy aliases (backward compatibility)
205
+ green: ansis.hex(brand.green),
206
+ red: ansis.hex(brand.red),
207
+ cyan: ansis.hex(brand.cyan),
208
+ slate: ansis.hex(brand.slate)
209
+ }
210
+ };
63
211
  }
64
212
 
65
213
  // Default export with auto-detected color support
66
- export const colors = createColors();
214
+ export let colors = createColors();
@@ -91,10 +91,7 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
91
91
  config.apiKey = token;
92
92
  config.projectSlug = projectMapping.projectSlug;
93
93
  config.organizationSlug = projectMapping.organizationSlug;
94
- output.debug('Using project mapping', {
95
- project: projectMapping.projectSlug,
96
- org: projectMapping.organizationSlug
97
- });
94
+ output.debug('config', `linked to ${projectMapping.projectSlug} (${projectMapping.organizationSlug})`);
98
95
  }
99
96
  }
100
97
 
@@ -104,14 +101,14 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
104
101
  const envParallelId = getParallelId();
105
102
  if (envApiKey) {
106
103
  config.apiKey = envApiKey;
107
- output.debug('Using API token from environment');
104
+ output.debug('config', 'using token from environment');
108
105
  }
109
106
  if (envApiUrl !== 'https://app.vizzly.dev') config.apiUrl = envApiUrl;
110
107
  if (envParallelId) config.parallelId = envParallelId;
111
108
 
112
109
  // 5. Apply CLI overrides (highest priority)
113
110
  if (cliOverrides.token) {
114
- output.debug('Using API token from --token flag');
111
+ output.debug('config', 'using token from --token flag');
115
112
  }
116
113
  applyCLIOverrides(config, cliOverrides);
117
114
  return config;
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Dynamic context detection for CLI commands
3
+ *
4
+ * Detects the current state of Vizzly in the working directory:
5
+ * - TDD server status
6
+ * - Project configuration
7
+ * - Authentication status
8
+ * - Baseline counts
9
+ */
10
+
11
+ import { existsSync, readFileSync } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import { dirname, join } from 'node:path';
14
+
15
+ /**
16
+ * Get dynamic context about the current Vizzly state
17
+ * Returns an array of context items with type, label, and value
18
+ *
19
+ * @returns {Array<{type: 'success'|'warning'|'info', label: string, value: string}>}
20
+ */
21
+ export function getContext() {
22
+ let items = [];
23
+ try {
24
+ let cwd = process.cwd();
25
+ let globalConfigPath = join(process.env.VIZZLY_HOME || join(homedir(), '.vizzly'), 'config.json');
26
+
27
+ // Load global config once
28
+ let globalConfig = {};
29
+ try {
30
+ if (existsSync(globalConfigPath)) {
31
+ globalConfig = JSON.parse(readFileSync(globalConfigPath, 'utf8'));
32
+ }
33
+ } catch {
34
+ // Ignore
35
+ }
36
+
37
+ // Check for vizzly.config.js (project config)
38
+ let hasProjectConfig = existsSync(join(cwd, 'vizzly.config.js'));
39
+
40
+ // Check for .vizzly directory (TDD baselines)
41
+ let baselineCount = 0;
42
+ try {
43
+ let metaPath = join(cwd, '.vizzly', 'baselines', 'metadata.json');
44
+ if (existsSync(metaPath)) {
45
+ let meta = JSON.parse(readFileSync(metaPath, 'utf8'));
46
+ baselineCount = meta.screenshots?.length || 0;
47
+ }
48
+ } catch {
49
+ // Ignore
50
+ }
51
+
52
+ // Check for TDD server running
53
+ let serverRunning = false;
54
+ let serverPort = null;
55
+ try {
56
+ let serverFile = join(cwd, '.vizzly', 'server.json');
57
+ if (existsSync(serverFile)) {
58
+ let serverInfo = JSON.parse(readFileSync(serverFile, 'utf8'));
59
+ serverPort = serverInfo.port;
60
+ serverRunning = true;
61
+ }
62
+ } catch {
63
+ // Ignore
64
+ }
65
+
66
+ // Check for project mapping (from vizzly project:select)
67
+ // Traverse up to find project config, with bounds check for Windows compatibility
68
+ let projectMapping = null;
69
+ let checkPath = cwd;
70
+ let prevPath = null;
71
+ while (checkPath && checkPath !== prevPath) {
72
+ if (globalConfig.projects?.[checkPath]) {
73
+ projectMapping = globalConfig.projects[checkPath];
74
+ break;
75
+ }
76
+ prevPath = checkPath;
77
+ checkPath = dirname(checkPath);
78
+ }
79
+
80
+ // Check for OAuth login (from vizzly login)
81
+ let isLoggedIn = !!globalConfig.auth?.accessToken;
82
+ let userName = globalConfig.auth?.user?.name || globalConfig.auth?.user?.email;
83
+
84
+ // Check for env token
85
+ let hasEnvToken = !!process.env.VIZZLY_TOKEN;
86
+
87
+ // Build context items - prioritize most useful info
88
+ if (serverRunning) {
89
+ items.push({
90
+ type: 'success',
91
+ label: 'TDD Server',
92
+ value: `running on :${serverPort}`
93
+ });
94
+ }
95
+ if (projectMapping) {
96
+ items.push({
97
+ type: 'success',
98
+ label: 'Project',
99
+ value: `${projectMapping.projectName} (${projectMapping.organizationSlug})`
100
+ });
101
+ } else if (isLoggedIn && userName) {
102
+ items.push({
103
+ type: 'success',
104
+ label: 'Logged in',
105
+ value: userName
106
+ });
107
+ } else if (hasEnvToken) {
108
+ items.push({
109
+ type: 'success',
110
+ label: 'API Token',
111
+ value: 'via VIZZLY_TOKEN'
112
+ });
113
+ } else {
114
+ items.push({
115
+ type: 'info',
116
+ label: 'Not connected',
117
+ value: 'run vizzly login or project:select'
118
+ });
119
+ }
120
+ if (baselineCount > 0) {
121
+ items.push({
122
+ type: 'success',
123
+ label: 'Baselines',
124
+ value: `${baselineCount} screenshots`
125
+ });
126
+ }
127
+ if (!hasProjectConfig && !serverRunning && baselineCount === 0) {
128
+ // Only show "no config" hint if there's nothing else useful
129
+ items.push({
130
+ type: 'info',
131
+ label: 'Get started',
132
+ value: 'run vizzly init'
133
+ });
134
+ }
135
+ } catch {
136
+ // If anything fails, just return empty - context is optional
137
+ }
138
+ return items;
139
+ }
140
+
141
+ /**
142
+ * Get detailed context with raw values (for doctor command)
143
+ * Returns more detailed information suitable for diagnostics
144
+ *
145
+ * @returns {Object} Detailed context object
146
+ */
147
+ export function getDetailedContext() {
148
+ let cwd = process.cwd();
149
+ let globalConfigPath = join(process.env.VIZZLY_HOME || join(homedir(), '.vizzly'), 'config.json');
150
+ let context = {
151
+ tddServer: {
152
+ running: false,
153
+ port: null
154
+ },
155
+ project: {
156
+ hasConfig: false,
157
+ mapping: null
158
+ },
159
+ auth: {
160
+ loggedIn: false,
161
+ userName: null,
162
+ hasEnvToken: false
163
+ },
164
+ baselines: {
165
+ count: 0,
166
+ path: null
167
+ }
168
+ };
169
+ try {
170
+ // Load global config
171
+ let globalConfig = {};
172
+ try {
173
+ if (existsSync(globalConfigPath)) {
174
+ globalConfig = JSON.parse(readFileSync(globalConfigPath, 'utf8'));
175
+ }
176
+ } catch {
177
+ // Ignore
178
+ }
179
+
180
+ // Check for vizzly.config.js
181
+ context.project.hasConfig = existsSync(join(cwd, 'vizzly.config.js'));
182
+
183
+ // Check for baselines
184
+ try {
185
+ let metaPath = join(cwd, '.vizzly', 'baselines', 'metadata.json');
186
+ if (existsSync(metaPath)) {
187
+ let meta = JSON.parse(readFileSync(metaPath, 'utf8'));
188
+ context.baselines.count = meta.screenshots?.length || 0;
189
+ context.baselines.path = join(cwd, '.vizzly', 'baselines');
190
+ }
191
+ } catch {
192
+ // Ignore
193
+ }
194
+
195
+ // Check for TDD server
196
+ try {
197
+ let serverFile = join(cwd, '.vizzly', 'server.json');
198
+ if (existsSync(serverFile)) {
199
+ let serverInfo = JSON.parse(readFileSync(serverFile, 'utf8'));
200
+ context.tddServer.running = true;
201
+ context.tddServer.port = serverInfo.port;
202
+ }
203
+ } catch {
204
+ // Ignore
205
+ }
206
+
207
+ // Check for project mapping
208
+ // Traverse up to find project config, with bounds check for Windows compatibility
209
+ let checkPath = cwd;
210
+ let prevPath = null;
211
+ while (checkPath && checkPath !== prevPath) {
212
+ if (globalConfig.projects?.[checkPath]) {
213
+ context.project.mapping = globalConfig.projects[checkPath];
214
+ break;
215
+ }
216
+ prevPath = checkPath;
217
+ checkPath = dirname(checkPath);
218
+ }
219
+
220
+ // Check auth status
221
+ context.auth.loggedIn = !!globalConfig.auth?.accessToken;
222
+ context.auth.userName = globalConfig.auth?.user?.name || globalConfig.auth?.user?.email;
223
+ context.auth.hasEnvToken = !!process.env.VIZZLY_TOKEN;
224
+ } catch {
225
+ // If anything fails, return defaults
226
+ }
227
+ return context;
228
+ }