@vizzly-testing/cli 0.20.0 → 0.20.1-beta.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 (84) hide show
  1. package/dist/api/client.js +134 -0
  2. package/dist/api/core.js +341 -0
  3. package/dist/api/endpoints.js +314 -0
  4. package/dist/api/index.js +19 -0
  5. package/dist/auth/client.js +91 -0
  6. package/dist/auth/core.js +176 -0
  7. package/dist/auth/index.js +30 -0
  8. package/dist/auth/operations.js +148 -0
  9. package/dist/cli.js +178 -3
  10. package/dist/client/index.js +144 -77
  11. package/dist/commands/doctor.js +121 -36
  12. package/dist/commands/finalize.js +49 -18
  13. package/dist/commands/init.js +13 -18
  14. package/dist/commands/login.js +49 -55
  15. package/dist/commands/logout.js +17 -9
  16. package/dist/commands/project.js +100 -71
  17. package/dist/commands/run.js +189 -95
  18. package/dist/commands/status.js +101 -66
  19. package/dist/commands/tdd-daemon.js +61 -32
  20. package/dist/commands/tdd.js +104 -98
  21. package/dist/commands/upload.js +78 -34
  22. package/dist/commands/whoami.js +44 -42
  23. package/dist/config/core.js +438 -0
  24. package/dist/config/index.js +13 -0
  25. package/dist/config/operations.js +327 -0
  26. package/dist/index.js +1 -1
  27. package/dist/project/core.js +295 -0
  28. package/dist/project/index.js +13 -0
  29. package/dist/project/operations.js +393 -0
  30. package/dist/reporter/reporter-bundle.css +1 -1
  31. package/dist/reporter/reporter-bundle.iife.js +16 -16
  32. package/dist/screenshot-server/core.js +157 -0
  33. package/dist/screenshot-server/index.js +11 -0
  34. package/dist/screenshot-server/operations.js +183 -0
  35. package/dist/sdk/index.js +3 -2
  36. package/dist/server/handlers/api-handler.js +14 -5
  37. package/dist/server/handlers/tdd-handler.js +191 -53
  38. package/dist/server/http-server.js +9 -3
  39. package/dist/server/routers/baseline.js +58 -0
  40. package/dist/server/routers/dashboard.js +10 -6
  41. package/dist/server/routers/screenshot.js +32 -0
  42. package/dist/server-manager/core.js +186 -0
  43. package/dist/server-manager/index.js +81 -0
  44. package/dist/server-manager/operations.js +209 -0
  45. package/dist/services/build-manager.js +2 -69
  46. package/dist/services/index.js +21 -48
  47. package/dist/services/screenshot-server.js +40 -74
  48. package/dist/services/server-manager.js +45 -80
  49. package/dist/services/test-runner.js +90 -250
  50. package/dist/services/uploader.js +56 -358
  51. package/dist/tdd/core/hotspot-coverage.js +112 -0
  52. package/dist/tdd/core/signature.js +101 -0
  53. package/dist/tdd/index.js +19 -0
  54. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  55. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  56. package/dist/tdd/services/baseline-downloader.js +151 -0
  57. package/dist/tdd/services/baseline-manager.js +166 -0
  58. package/dist/tdd/services/comparison-service.js +230 -0
  59. package/dist/tdd/services/hotspot-service.js +71 -0
  60. package/dist/tdd/services/result-service.js +123 -0
  61. package/dist/tdd/tdd-service.js +1145 -0
  62. package/dist/test-runner/core.js +255 -0
  63. package/dist/test-runner/index.js +13 -0
  64. package/dist/test-runner/operations.js +483 -0
  65. package/dist/types/client.d.ts +25 -2
  66. package/dist/uploader/core.js +396 -0
  67. package/dist/uploader/index.js +11 -0
  68. package/dist/uploader/operations.js +412 -0
  69. package/dist/utils/colors.js +187 -39
  70. package/dist/utils/config-loader.js +3 -6
  71. package/dist/utils/context.js +228 -0
  72. package/dist/utils/output.js +449 -14
  73. package/docs/api-reference.md +173 -8
  74. package/docs/tui-elements.md +560 -0
  75. package/package.json +13 -13
  76. package/dist/services/api-service.js +0 -412
  77. package/dist/services/auth-service.js +0 -226
  78. package/dist/services/config-service.js +0 -369
  79. package/dist/services/html-report-generator.js +0 -455
  80. package/dist/services/project-service.js +0 -326
  81. package/dist/services/report-generator/report.css +0 -411
  82. package/dist/services/report-generator/viewer.js +0 -102
  83. package/dist/services/static-report-generator.js +0 -207
  84. package/dist/services/tdd-service.js +0 -1437
@@ -3,9 +3,8 @@
3
3
  * Shows current user and authentication status
4
4
  */
5
5
 
6
- import { AuthService } from '../services/auth-service.js';
6
+ import { createAuthClient, createTokenStore, getAuthTokens, whoami } from '../auth/index.js';
7
7
  import { getApiUrl } from '../utils/environment-config.js';
8
- import { getAuthTokens } from '../utils/global-config.js';
9
8
  import * as output from '../utils/output.js';
10
9
 
11
10
  /**
@@ -21,16 +20,17 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
21
20
  });
22
21
  try {
23
22
  // Check if user is logged in
24
- const auth = await getAuthTokens();
23
+ let auth = await getAuthTokens();
25
24
  if (!auth || !auth.accessToken) {
26
25
  if (globalOptions.json) {
27
26
  output.data({
28
27
  authenticated: false
29
28
  });
30
29
  } else {
31
- output.info('You are not logged in');
30
+ output.header('whoami');
31
+ output.print(' Not logged in');
32
32
  output.blank();
33
- output.info('Run "vizzly login" to authenticate');
33
+ output.hint('Run "vizzly login" to authenticate');
34
34
  }
35
35
  output.cleanup();
36
36
  return;
@@ -38,10 +38,11 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
38
38
 
39
39
  // Get current user info
40
40
  output.startSpinner('Fetching user information...');
41
- const authService = new AuthService({
41
+ let client = createAuthClient({
42
42
  baseUrl: options.apiUrl || getApiUrl()
43
43
  });
44
- const response = await authService.whoami();
44
+ let tokenStore = createTokenStore();
45
+ let response = await whoami(client, tokenStore);
45
46
  output.stopSpinner();
46
47
 
47
48
  // Output in JSON mode
@@ -57,36 +58,39 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
57
58
  }
58
59
 
59
60
  // Human-readable output
60
- output.success('Authenticated');
61
- output.blank();
61
+ output.header('whoami');
62
62
 
63
- // Show user info
63
+ // Show user info using keyValue
64
64
  if (response.user) {
65
- output.info(`User: ${response.user.name || response.user.username}`);
66
- output.info(`Email: ${response.user.email}`);
65
+ let userInfo = {
66
+ User: response.user.name || response.user.username,
67
+ Email: response.user.email
68
+ };
67
69
  if (response.user.username) {
68
- output.info(`Username: ${response.user.username}`);
70
+ userInfo.Username = response.user.username;
69
71
  }
70
72
  if (globalOptions.verbose && response.user.id) {
71
- output.info(`User ID: ${response.user.id}`);
73
+ userInfo['User ID'] = response.user.id;
72
74
  }
75
+ output.keyValue(userInfo);
73
76
  }
74
77
 
75
- // Show organizations
78
+ // Show organizations as a list
76
79
  if (response.organizations && response.organizations.length > 0) {
77
80
  output.blank();
78
- output.info('Organizations:');
79
- for (const org of response.organizations) {
80
- let orgInfo = ` - ${org.name}`;
81
- if (org.slug) {
82
- orgInfo += ` (@${org.slug})`;
83
- }
84
- if (org.role) {
85
- orgInfo += ` [${org.role}]`;
86
- }
87
- output.print(orgInfo);
88
- if (globalOptions.verbose && org.id) {
89
- output.print(` ID: ${org.id}`);
81
+ output.labelValue('Organizations', '');
82
+ let orgItems = response.organizations.map(org => {
83
+ let parts = [org.name];
84
+ if (org.slug) parts.push(`@${org.slug}`);
85
+ if (org.role) parts.push(`[${org.role}]`);
86
+ return parts.join(' ');
87
+ });
88
+ output.list(orgItems);
89
+ if (globalOptions.verbose) {
90
+ for (let org of response.organizations) {
91
+ if (org.id) {
92
+ output.hint(` ${org.name} ID: ${org.id}`);
93
+ }
90
94
  }
91
95
  }
92
96
  }
@@ -94,29 +98,27 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
94
98
  // Show token expiry info
95
99
  if (auth.expiresAt) {
96
100
  output.blank();
97
- const expiresAt = new Date(auth.expiresAt);
98
- const now = new Date();
99
- const msUntilExpiry = expiresAt.getTime() - now.getTime();
100
- const daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
101
- const hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
102
- const minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
101
+ let expiresAt = new Date(auth.expiresAt);
102
+ let now = new Date();
103
+ let msUntilExpiry = expiresAt.getTime() - now.getTime();
104
+ let daysUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60 * 24));
105
+ let hoursUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60 * 60));
106
+ let minutesUntilExpiry = Math.floor(msUntilExpiry / (1000 * 60));
103
107
  if (msUntilExpiry <= 0) {
104
108
  output.warn('Token has expired');
105
- output.blank();
106
- output.info('Run "vizzly login" to refresh your authentication');
109
+ output.hint('Run "vizzly login" to refresh your authentication');
107
110
  } else if (daysUntilExpiry > 0) {
108
- output.info(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleDateString()})`);
111
+ output.hint(`Token expires in ${daysUntilExpiry} day${daysUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleDateString()})`);
109
112
  } else if (hoursUntilExpiry > 0) {
110
- output.info(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''} (${expiresAt.toLocaleString()})`);
113
+ output.hint(`Token expires in ${hoursUntilExpiry} hour${hoursUntilExpiry !== 1 ? 's' : ''}`);
111
114
  } else if (minutesUntilExpiry > 0) {
112
- output.info(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
115
+ output.hint(`Token expires in ${minutesUntilExpiry} minute${minutesUntilExpiry !== 1 ? 's' : ''}`);
113
116
  } else {
114
117
  output.warn('Token expires in less than a minute');
115
- output.blank();
116
- output.info('Run "vizzly login" to refresh your authentication');
118
+ output.hint('Run "vizzly login" to refresh your authentication');
117
119
  }
118
120
  if (globalOptions.verbose) {
119
- output.info(`Token expires at: ${expiresAt.toISOString()}`);
121
+ output.hint(`Token expires at: ${expiresAt.toISOString()}`);
120
122
  }
121
123
  }
122
124
  output.cleanup();
@@ -133,7 +135,7 @@ export async function whoamiCommand(options = {}, globalOptions = {}) {
133
135
  } else {
134
136
  output.error('Authentication token is invalid or expired', error);
135
137
  output.blank();
136
- output.info('Run "vizzly login" to authenticate again');
138
+ output.hint('Run "vizzly login" to authenticate again');
137
139
  }
138
140
  output.cleanup();
139
141
  process.exit(1);
@@ -0,0 +1,438 @@
1
+ /**
2
+ * Config Core - Pure functions for configuration logic
3
+ *
4
+ * No I/O, no side effects - just data transformations.
5
+ */
6
+
7
+ import { VizzlyError } from '../errors/vizzly-error.js';
8
+
9
+ // ============================================================================
10
+ // Default Configuration
11
+ // ============================================================================
12
+
13
+ /**
14
+ * Default configuration values
15
+ */
16
+ export const CONFIG_DEFAULTS = {
17
+ apiUrl: 'https://app.vizzly.dev',
18
+ server: {
19
+ port: 47392,
20
+ timeout: 30000
21
+ },
22
+ build: {
23
+ name: 'Build {timestamp}',
24
+ environment: 'test'
25
+ },
26
+ upload: {
27
+ screenshotsDir: './screenshots',
28
+ batchSize: 10,
29
+ timeout: 30000
30
+ },
31
+ comparison: {
32
+ threshold: 2.0
33
+ },
34
+ tdd: {
35
+ openReport: false
36
+ },
37
+ plugins: []
38
+ };
39
+
40
+ /**
41
+ * Valid config scopes for reading
42
+ */
43
+ export const READ_SCOPES = ['project', 'global', 'merged'];
44
+
45
+ /**
46
+ * Valid config scopes for writing
47
+ */
48
+ export const WRITE_SCOPES = ['project', 'global'];
49
+
50
+ // ============================================================================
51
+ // Scope Validation
52
+ // ============================================================================
53
+
54
+ /**
55
+ * Validate that a scope is valid for reading
56
+ * @param {string} scope - Scope to validate
57
+ * @returns {{ valid: boolean, error: Error|null }}
58
+ */
59
+ export function validateReadScope(scope) {
60
+ if (!READ_SCOPES.includes(scope)) {
61
+ return {
62
+ valid: false,
63
+ error: new VizzlyError(`Invalid config scope: ${scope}. Must be 'project', 'global', or 'merged'`, 'INVALID_CONFIG_SCOPE')
64
+ };
65
+ }
66
+ return {
67
+ valid: true,
68
+ error: null
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Validate that a scope is valid for writing
74
+ * @param {string} scope - Scope to validate
75
+ * @returns {{ valid: boolean, error: Error|null }}
76
+ */
77
+ export function validateWriteScope(scope) {
78
+ if (!WRITE_SCOPES.includes(scope)) {
79
+ return {
80
+ valid: false,
81
+ error: new VizzlyError(`Invalid config scope for update: ${scope}. Must be 'project' or 'global'`, 'INVALID_CONFIG_SCOPE')
82
+ };
83
+ }
84
+ return {
85
+ valid: true,
86
+ error: null
87
+ };
88
+ }
89
+
90
+ // ============================================================================
91
+ // Deep Merge
92
+ // ============================================================================
93
+
94
+ /**
95
+ * Deep merge two objects
96
+ * @param {Object} target - Target object
97
+ * @param {Object} source - Source object
98
+ * @returns {Object} Merged object (new object, inputs not mutated)
99
+ */
100
+ export function deepMerge(target, source) {
101
+ let output = {
102
+ ...target
103
+ };
104
+ for (let key of Object.keys(source)) {
105
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
106
+ if (target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])) {
107
+ output[key] = deepMerge(target[key], source[key]);
108
+ } else {
109
+ output[key] = source[key];
110
+ }
111
+ } else {
112
+ output[key] = source[key];
113
+ }
114
+ }
115
+ return output;
116
+ }
117
+
118
+ // ============================================================================
119
+ // Config Merging with Source Tracking
120
+ // ============================================================================
121
+
122
+ /**
123
+ * Ensure value is a plain object, return empty object otherwise
124
+ * @param {*} value - Value to check
125
+ * @returns {Object} The value if it's an object, empty object otherwise
126
+ */
127
+ function ensureObject(value) {
128
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
129
+ return value;
130
+ }
131
+ return {};
132
+ }
133
+
134
+ /**
135
+ * Build merged config from layers with source tracking
136
+ * @param {Object} options - Config layers
137
+ * @param {Object} options.projectConfig - Project config (from vizzly.config.js)
138
+ * @param {Object} options.globalConfig - Global config (from ~/.vizzly/config.json)
139
+ * @param {Object} [options.envOverrides] - Environment variable overrides
140
+ * @returns {{ config: Object, sources: Object }}
141
+ */
142
+ export function buildMergedConfig({
143
+ projectConfig = {},
144
+ globalConfig = {},
145
+ envOverrides = {}
146
+ } = {}) {
147
+ // Ensure all inputs are plain objects
148
+ let safeProjectConfig = ensureObject(projectConfig);
149
+ let safeGlobalConfig = ensureObject(globalConfig);
150
+ let safeEnvOverrides = ensureObject(envOverrides);
151
+ let mergedConfig = {};
152
+ let sources = {};
153
+
154
+ // Layer 1: Defaults
155
+ for (let key of Object.keys(CONFIG_DEFAULTS)) {
156
+ mergedConfig[key] = CONFIG_DEFAULTS[key];
157
+ sources[key] = 'default';
158
+ }
159
+
160
+ // Layer 2: Global config (auth, project mappings, user preferences)
161
+ if (safeGlobalConfig.auth) {
162
+ mergedConfig.auth = safeGlobalConfig.auth;
163
+ sources.auth = 'global';
164
+ }
165
+ if (safeGlobalConfig.projects) {
166
+ mergedConfig.projects = safeGlobalConfig.projects;
167
+ sources.projects = 'global';
168
+ }
169
+
170
+ // Layer 3: Project config file
171
+ for (let key of Object.keys(safeProjectConfig)) {
172
+ mergedConfig[key] = safeProjectConfig[key];
173
+ sources[key] = 'project';
174
+ }
175
+
176
+ // Layer 4: Environment variables
177
+ for (let key of Object.keys(safeEnvOverrides)) {
178
+ mergedConfig[key] = safeEnvOverrides[key];
179
+ sources[key] = 'env';
180
+ }
181
+ return {
182
+ config: mergedConfig,
183
+ sources
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Extract environment variable overrides
189
+ * @param {Object} env - Environment variables object (defaults to process.env)
190
+ * @returns {Object} Overrides from environment
191
+ */
192
+ export function extractEnvOverrides(env = process.env) {
193
+ let overrides = {};
194
+ if (env.VIZZLY_TOKEN) {
195
+ overrides.apiKey = env.VIZZLY_TOKEN;
196
+ }
197
+ if (env.VIZZLY_API_URL) {
198
+ overrides.apiUrl = env.VIZZLY_API_URL;
199
+ }
200
+ return overrides;
201
+ }
202
+
203
+ // ============================================================================
204
+ // Config Result Building
205
+ // ============================================================================
206
+
207
+ /**
208
+ * Build a project config result object
209
+ * @param {Object|null} config - Config object or null if not found
210
+ * @param {string|null} filepath - Path to config file or null
211
+ * @returns {{ config: Object, filepath: string|null, isEmpty: boolean }}
212
+ */
213
+ export function buildProjectConfigResult(config, filepath) {
214
+ if (!config) {
215
+ return {
216
+ config: {},
217
+ filepath: null,
218
+ isEmpty: true
219
+ };
220
+ }
221
+ return {
222
+ config,
223
+ filepath,
224
+ isEmpty: Object.keys(config).length === 0
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Build a global config result object
230
+ * @param {Object} config - Global config object
231
+ * @param {string} filepath - Path to global config file
232
+ * @returns {{ config: Object, filepath: string, isEmpty: boolean }}
233
+ */
234
+ export function buildGlobalConfigResult(config, filepath) {
235
+ return {
236
+ config,
237
+ filepath,
238
+ isEmpty: Object.keys(config).length === 0
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Build a merged config result object
244
+ * @param {Object} options - Build options
245
+ * @returns {{ config: Object, sources: Object, projectFilepath: string|null, globalFilepath: string }}
246
+ */
247
+ export function buildMergedConfigResult({
248
+ projectConfig,
249
+ globalConfig,
250
+ envOverrides,
251
+ projectFilepath,
252
+ globalFilepath
253
+ }) {
254
+ let {
255
+ config,
256
+ sources
257
+ } = buildMergedConfig({
258
+ projectConfig,
259
+ globalConfig,
260
+ envOverrides
261
+ });
262
+ return {
263
+ config,
264
+ sources,
265
+ projectFilepath,
266
+ globalFilepath
267
+ };
268
+ }
269
+
270
+ // ============================================================================
271
+ // Config Serialization
272
+ // ============================================================================
273
+
274
+ /**
275
+ * Stringify a value with proper indentation for JavaScript output
276
+ * @param {*} value - Value to stringify
277
+ * @param {number} depth - Current depth for indentation
278
+ * @returns {string} JavaScript representation of value
279
+ */
280
+ export function stringifyWithIndent(value, depth = 0) {
281
+ let indent = ' '.repeat(depth);
282
+ let prevIndent = depth > 0 ? ' '.repeat(depth - 1) : '';
283
+ if (value === null || value === undefined) {
284
+ return String(value);
285
+ }
286
+ if (typeof value === 'string') {
287
+ return `'${value.replace(/'/g, "\\'")}'`;
288
+ }
289
+ if (typeof value === 'number' || typeof value === 'boolean') {
290
+ return String(value);
291
+ }
292
+ if (Array.isArray(value)) {
293
+ if (value.length === 0) return '[]';
294
+ let items = value.map(item => `${indent}${stringifyWithIndent(item, depth + 1)}`);
295
+ return `[\n${items.join(',\n')}\n${prevIndent}]`;
296
+ }
297
+ if (typeof value === 'object') {
298
+ let keys = Object.keys(value);
299
+ if (keys.length === 0) return '{}';
300
+ let items = keys.map(key => {
301
+ let val = stringifyWithIndent(value[key], depth + 1);
302
+ return `${indent}${key}: ${val}`;
303
+ });
304
+ return `{\n${items.join(',\n')}\n${prevIndent}}`;
305
+ }
306
+ return String(value);
307
+ }
308
+
309
+ /**
310
+ * Serialize config to JavaScript module format
311
+ * @param {Object} config - Config object to serialize
312
+ * @returns {string} JavaScript source code
313
+ */
314
+ export function serializeToJavaScript(config) {
315
+ let lines = ['/**', ' * Vizzly Configuration', ' * @see https://docs.vizzly.dev/cli/configuration', ' */', '', "import { defineConfig } from '@vizzly-testing/cli/config';", '', 'export default defineConfig(', stringifyWithIndent(config, 1), ');', ''];
316
+ return lines.join('\n');
317
+ }
318
+
319
+ /**
320
+ * Serialize config to JSON format
321
+ * @param {Object} config - Config object to serialize
322
+ * @returns {string} JSON string with 2-space indentation
323
+ */
324
+ export function serializeToJson(config) {
325
+ return JSON.stringify(config, null, 2);
326
+ }
327
+
328
+ /**
329
+ * Determine the serialization format based on filepath
330
+ * @param {string} filepath - Path to config file
331
+ * @returns {'javascript'|'json'|'package'|'unknown'} Format type
332
+ */
333
+ export function getConfigFormat(filepath) {
334
+ if (filepath.endsWith('.js') || filepath.endsWith('.mjs')) {
335
+ return 'javascript';
336
+ }
337
+ if (filepath.endsWith('.json') && !filepath.endsWith('package.json')) {
338
+ return 'json';
339
+ }
340
+ if (filepath.endsWith('package.json')) {
341
+ return 'package';
342
+ }
343
+ return 'unknown';
344
+ }
345
+
346
+ /**
347
+ * Serialize config for writing to file
348
+ * @param {Object} config - Config object to serialize
349
+ * @param {string} filepath - Target file path
350
+ * @returns {{ content: string|null, format: string, error: Error|null }}
351
+ */
352
+ export function serializeConfig(config, filepath) {
353
+ let format = getConfigFormat(filepath);
354
+ if (format === 'javascript') {
355
+ return {
356
+ content: serializeToJavaScript(config),
357
+ format,
358
+ error: null
359
+ };
360
+ }
361
+ if (format === 'json') {
362
+ return {
363
+ content: serializeToJson(config),
364
+ format,
365
+ error: null
366
+ };
367
+ }
368
+ if (format === 'package') {
369
+ // Can't serialize standalone, need existing package.json
370
+ return {
371
+ content: null,
372
+ format,
373
+ error: null
374
+ };
375
+ }
376
+ return {
377
+ content: null,
378
+ format,
379
+ error: new VizzlyError(`Unsupported config file format: ${filepath}`, 'UNSUPPORTED_CONFIG_FORMAT')
380
+ };
381
+ }
382
+
383
+ // ============================================================================
384
+ // Config Extraction
385
+ // ============================================================================
386
+
387
+ /**
388
+ * Extract config from cosmiconfig result (handles .default exports)
389
+ * @param {Object|null} result - Cosmiconfig result
390
+ * @returns {{ config: Object|null, filepath: string|null }}
391
+ */
392
+ export function extractCosmiconfigResult(result) {
393
+ if (!result || !result.config) {
394
+ return {
395
+ config: null,
396
+ filepath: null
397
+ };
398
+ }
399
+
400
+ // Handle both `export default` and `module.exports`
401
+ let config = result.config.default || result.config;
402
+ return {
403
+ config,
404
+ filepath: result.filepath
405
+ };
406
+ }
407
+
408
+ // ============================================================================
409
+ // Validation Result Building
410
+ // ============================================================================
411
+
412
+ /**
413
+ * Build a validation success result
414
+ * @param {Object} validatedConfig - Validated config
415
+ * @returns {{ valid: true, config: Object, errors: [] }}
416
+ */
417
+ export function buildValidationSuccess(validatedConfig) {
418
+ return {
419
+ valid: true,
420
+ config: validatedConfig,
421
+ errors: []
422
+ };
423
+ }
424
+
425
+ /**
426
+ * Build a validation failure result
427
+ * @param {Error} error - Validation error
428
+ * @returns {{ valid: false, config: null, errors: Array }}
429
+ */
430
+ export function buildValidationFailure(error) {
431
+ return {
432
+ valid: false,
433
+ config: null,
434
+ errors: error.errors || [{
435
+ message: error.message
436
+ }]
437
+ };
438
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Config Module - Public exports
3
+ *
4
+ * Provides functional configuration primitives:
5
+ * - core.js: Pure functions for merging, serialization, validation results
6
+ * - operations.js: Config operations with dependency injection
7
+ */
8
+
9
+ // Core pure functions
10
+ export { buildGlobalConfigResult, buildMergedConfig, buildMergedConfigResult, buildProjectConfigResult, buildValidationFailure, buildValidationSuccess, CONFIG_DEFAULTS, deepMerge, extractCosmiconfigResult, extractEnvOverrides, getConfigFormat, READ_SCOPES, serializeConfig, serializeToJavaScript, serializeToJson, stringifyWithIndent, validateReadScope, validateWriteScope, WRITE_SCOPES } from './core.js';
11
+
12
+ // Config operations (take dependencies as parameters)
13
+ export { getConfig, getConfigSource, getGlobalConfig, getMergedConfig, getProjectConfig, updateConfig, updateGlobalConfig, updateProjectConfig, validateConfig, writeProjectConfigFile } from './operations.js';