@vizzly-testing/cli 0.39.1 → 0.39.2
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.
- package/README.md +8 -0
- package/bin/vizzly.js +1 -1
- package/dist/{cli.js → adapters/cli/cli.js} +41 -35
- package/dist/adapters/cli/setup.js +34 -0
- package/dist/{client → adapters/client}/index.js +6 -7
- package/dist/adapters/http/cloud-proxy.js +29 -0
- package/dist/{server → adapters/http}/http-server.js +12 -13
- package/dist/{server-manager/operations.js → adapters/http/server-lifecycle.js} +10 -13
- package/dist/{server-manager/index.js → adapters/http/server-manager.js} +23 -27
- package/dist/{plugin-api.js → adapters/plugins/plugin-api.js} +1 -1
- package/dist/{plugin-loader.js → adapters/plugins/plugin-loader.js} +1 -1
- package/dist/{services/index.js → adapters/plugins/services.js} +4 -9
- package/dist/adapters/sdk/index.d.ts +89 -0
- package/dist/{sdk → adapters/sdk}/index.js +22 -19
- package/dist/{commands/api.js → domains/api/api.commands.js} +3 -3
- package/dist/{auth/client.js → domains/auth/auth-client.js} +4 -3
- package/dist/domains/auth/auth-errors.js +50 -0
- package/dist/domains/auth/auth-headers.js +41 -0
- package/dist/domains/auth/auth.normalizers.js +50 -0
- package/dist/{server/routers/auth.js → domains/auth/auth.routes.js} +9 -9
- package/dist/domains/auth/auth.rules.js +19 -0
- package/dist/domains/auth/auth.serializers.js +32 -0
- package/dist/{auth/operations.js → domains/auth/auth.service.js} +115 -17
- package/dist/domains/auth/auth.validations.js +21 -0
- package/dist/{api/client.js → domains/auth/cloud-client.js} +11 -18
- package/dist/{commands/login.js → domains/auth/login.commands.js} +11 -8
- package/dist/{commands/logout.js → domains/auth/logout.commands.js} +6 -4
- package/dist/domains/auth/token-store.js +81 -0
- package/dist/{commands/whoami.js → domains/auth/whoami.commands.js} +6 -3
- package/dist/domains/baselines/baseline-download.js +192 -0
- package/dist/domains/baselines/baseline-write.js +22 -0
- package/dist/{server/routers/baseline.js → domains/baselines/baseline.routes.js} +3 -3
- package/dist/{tdd/services/baseline-manager.js → domains/baselines/baseline.service.js} +1 -39
- package/dist/{commands/baselines.js → domains/baselines/baselines.commands.js} +3 -3
- package/dist/domains/baselines/cloud-baselines.js +9 -0
- package/dist/domains/baselines/cloud-baselines.routes.js +42 -0
- package/dist/domains/builds/build.normalizers.js +35 -0
- package/dist/domains/builds/build.serializers.js +164 -0
- package/dist/domains/builds/build.service.js +119 -0
- package/dist/domains/builds/build.validations.js +39 -0
- package/dist/{commands/builds.js → domains/builds/builds.commands.js} +6 -74
- package/dist/domains/builds/cloud-builds.routes.js +31 -0
- package/dist/{commands/finalize.js → domains/builds/finalize.commands.js} +5 -17
- package/dist/{commands/status.js → domains/builds/status.commands.js} +9 -120
- package/dist/domains/comparisons/cloud-comparisons.js +43 -0
- package/dist/{tdd/services/comparison-service.js → domains/comparisons/comparison.service.js} +1 -1
- package/dist/{commands/comparisons.js → domains/comparisons/comparisons.commands.js} +7 -5
- package/dist/domains/config/config-defaults.js +27 -0
- package/dist/{utils → domains/config}/config-discovery.js +1 -9
- package/dist/{utils → domains/config}/config-loader.js +10 -9
- package/dist/{commands/config-cmd.js → domains/config/config.commands.js} +4 -4
- package/dist/{types → domains/config}/config.d.ts +3 -3
- package/dist/domains/config/config.normalizers.js +126 -0
- package/dist/{server/routers/config.js → domains/config/config.routes.js} +13 -13
- package/dist/domains/config/config.rules.js +47 -0
- package/dist/domains/config/config.serializers.js +202 -0
- package/dist/domains/config/config.service.js +625 -0
- package/dist/{utils/config-schema.js → domains/config/config.validations.js} +9 -9
- package/dist/{utils/environment-config.js → domains/config/environment.js} +1 -8
- package/dist/domains/context/compact-context.validations.js +60 -0
- package/dist/domains/context/context-presentation.js +368 -0
- package/dist/domains/context/context.commands.js +385 -0
- package/dist/domains/context/context.serializers.js +157 -0
- package/dist/domains/context/context.service.js +76 -0
- package/dist/domains/context/context.validations.js +100 -0
- package/dist/{context → domains/context}/local-workspace-provider.js +1 -1
- package/dist/{commands/doctor.js → domains/diagnostics/doctor.commands.js} +6 -5
- package/dist/{server/routers/health.js → domains/diagnostics/health.routes.js} +11 -20
- package/dist/{commands/preview.js → domains/previews/preview.commands.js} +9 -7
- package/dist/domains/previews/preview.service.js +37 -0
- package/dist/domains/projects/cloud-projects.routes.js +28 -0
- package/dist/{commands/orgs.js → domains/projects/orgs.commands.js} +6 -8
- package/dist/{utils → domains/projects}/project-binding.js +3 -3
- package/dist/domains/projects/project-errors.js +61 -0
- package/dist/{utils → domains/projects}/project-link-store.js +2 -2
- package/dist/domains/projects/project-requests.js +61 -0
- package/dist/{commands/project.js → domains/projects/project.commands.js} +9 -41
- package/dist/domains/projects/project.normalizers.js +69 -0
- package/dist/{project/operations.js → domains/projects/project.service.js} +89 -16
- package/dist/domains/projects/project.validations.js +31 -0
- package/dist/{commands/projects.js → domains/projects/projects.commands.js} +6 -8
- package/dist/{server/routers/projects.js → domains/projects/projects.routes.js} +5 -5
- package/dist/domains/reports/dashboard.routes.js +96 -0
- package/dist/{server/routers/events.js → domains/reports/events.routes.js} +14 -36
- package/dist/domains/reports/report-assets.routes.js +81 -0
- package/dist/domains/reports/report-state.js +51 -0
- package/dist/domains/reports/report-store.js +85 -0
- package/dist/domains/reports/report.serializers.js +99 -0
- package/dist/{services/static-report-generator.js → domains/reports/report.service.js} +11 -33
- package/dist/{commands/review.js → domains/review/review.commands.js} +4 -4
- package/dist/domains/runs/run-environment.js +69 -0
- package/dist/{services/test-runner.js → domains/runs/run-events.js} +11 -9
- package/dist/{commands/run.js → domains/runs/run.commands.js} +16 -13
- package/dist/domains/runs/run.normalizers.js +8 -0
- package/dist/domains/runs/run.rules.js +56 -0
- package/dist/domains/runs/run.serializers.js +101 -0
- package/dist/{test-runner/operations.js → domains/runs/run.service.js} +35 -7
- package/dist/domains/runs/run.validations.js +17 -0
- package/dist/{utils/session.js → domains/runs/session-store.js} +3 -2
- package/dist/{server/handlers/api-handler.js → domains/screenshots/cloud-handler.js} +4 -4
- package/dist/domains/screenshots/cloud-screenshots.js +109 -0
- package/dist/{utils/file-helpers.js → domains/screenshots/image-files.js} +1 -1
- package/dist/domains/screenshots/screenshot-hash.js +53 -0
- package/dist/domains/screenshots/screenshot-server-options.js +28 -0
- package/dist/{screenshot-server/operations.js → domains/screenshots/screenshot-server.js} +14 -1
- package/dist/domains/screenshots/screenshot.normalizers.js +40 -0
- package/dist/{server/routers/screenshot.js → domains/screenshots/screenshot.routes.js} +4 -4
- package/dist/domains/screenshots/screenshot.serializers.js +146 -0
- package/dist/{utils/security.js → domains/screenshots/screenshot.validations.js} +20 -70
- package/dist/{init/agents.js → domains/setup/agent-setup.js} +4 -5
- package/dist/{commands/init.js → domains/setup/init.commands.js} +10 -41
- package/dist/{server/handlers/tdd-handler.js → domains/tdd/screenshot-handler.js} +46 -281
- package/dist/{commands/tdd-daemon.js → domains/tdd/tdd-daemon.commands.js} +6 -6
- package/dist/{commands/tdd-screenshots.js → domains/tdd/tdd-screenshots.commands.js} +5 -5
- package/dist/{commands/tdd.js → domains/tdd/tdd.commands.js} +16 -15
- package/dist/{tdd/tdd-service.js → domains/tdd/tdd.service.js} +81 -405
- package/dist/{utils → domains/updates}/cli-update.js +4 -6
- package/dist/{utils → domains/updates}/update-notifications.js +7 -7
- package/dist/domains/uploads/upload-defaults.js +4 -0
- package/dist/domains/uploads/upload-files.js +32 -0
- package/dist/domains/uploads/upload-settings.js +21 -0
- package/dist/domains/uploads/upload-timing.js +18 -0
- package/dist/{commands/upload.js → domains/uploads/upload.commands.js} +11 -9
- package/dist/domains/uploads/upload.normalizers.js +60 -0
- package/dist/domains/uploads/upload.serializers.js +179 -0
- package/dist/{uploader/operations.js → domains/uploads/upload.service.js} +85 -28
- package/dist/domains/uploads/upload.validations.js +77 -0
- package/dist/index.js +12 -12
- package/dist/reporter/reporter-bundle.iife.js +86 -86
- package/dist/reporter-ssr/ssr-entry.js +2 -2
- package/dist/shared/cloud-protocol.js +171 -0
- package/dist/{utils → shared}/file-changes.js +1 -1
- package/dist/shared/files.js +9 -0
- package/dist/{utils → shared}/global-config.js +7 -78
- package/dist/shared/log-level.js +7 -0
- package/dist/{utils → shared}/output.js +1 -1
- package/dist/{utils → shared}/package-info.js +5 -4
- package/dist/shared/paths.js +57 -0
- package/package.json +23 -22
- package/dist/api/core.js +0 -382
- package/dist/api/endpoints.js +0 -421
- package/dist/api/index.js +0 -19
- package/dist/auth/core.js +0 -237
- package/dist/auth/index.js +0 -31
- package/dist/commands/context.js +0 -1066
- package/dist/config/core.js +0 -439
- package/dist/config/operations.js +0 -327
- package/dist/project/core.js +0 -211
- package/dist/screenshot-server/core.js +0 -161
- package/dist/screenshot-server/index.js +0 -11
- package/dist/server/routers/assets.js +0 -95
- package/dist/server/routers/cloud-proxy.js +0 -146
- package/dist/server/routers/dashboard.js +0 -172
- package/dist/services/auth-service.js +0 -117
- package/dist/services/config-service.js +0 -313
- package/dist/services/project-service.js +0 -88
- package/dist/test-runner/core.js +0 -293
- package/dist/test-runner/index.js +0 -13
- package/dist/types/sdk.d.ts +0 -186
- package/dist/uploader/core.js +0 -432
- package/dist/uploader/index.js +0 -88
- /package/dist/{types/client.d.ts → adapters/client/index.d.ts} +0 -0
- /package/dist/{server → adapters/http}/middleware/cors.js +0 -0
- /package/dist/{server → adapters/http}/middleware/json-parser.js +0 -0
- /package/dist/{server → adapters/http}/middleware/response.js +0 -0
- /package/dist/{server-manager/core.js → adapters/http/server-options.js} +0 -0
- /package/dist/{utils/login-poll-wait.js → domains/auth/login-wait.js} +0 -0
- /package/dist/{tdd/metadata → domains/baselines}/baseline-metadata.js +0 -0
- /package/dist/{services/build-manager.js → domains/builds/build-info.js} +0 -0
- /package/dist/{tdd/services/result-service.js → domains/comparisons/comparison-results.js} +0 -0
- /package/dist/{init/core.js → domains/config/config-plan.js} +0 -0
- /package/dist/{utils/config-helpers.js → domains/config/config.js} +0 -0
- /package/dist/{utils/visual-context-normalizers.js → domains/context/context.normalizers.js} +0 -0
- /package/dist/{context → domains/context}/provider-resolver.js +0 -0
- /package/dist/{utils/context.js → domains/diagnostics/environment-context.js} +0 -0
- /package/dist/{utils/report-data.js → domains/reports/report.normalizers.js} +0 -0
- /package/dist/{utils/image-input-detector.js → domains/screenshots/image-input.js} +0 -0
- /package/dist/{tdd/core/signature.js → domains/screenshots/screenshot-identity.js} +0 -0
- /package/dist/{utils → domains/screenshots}/screenshot-options.js +0 -0
- /package/dist/{init/prompts.js → domains/setup/setup-prompts.js} +0 -0
- /package/dist/{tdd → domains/tdd}/runtime-context.js +0 -0
- /package/dist/{tdd → domains/tdd}/server-registry.js +0 -0
- /package/dist/{commands/tdd-daemon-process.js → domains/tdd/tdd-daemon-process.commands.js} +0 -0
- /package/dist/{utils → domains/updates}/update-check.js +0 -0
- /package/dist/{types/index.d.ts → index.d.ts} +0 -0
- /package/dist/{utils → shared}/api-url.js +0 -0
- /package/dist/{utils → shared}/async-utils.js +0 -0
- /package/dist/{utils → shared}/browser.js +0 -0
- /package/dist/{utils → shared}/ci-env.js +0 -0
- /package/dist/{utils → shared}/colors.js +0 -0
- /package/dist/{utils → shared}/fetch-utils.js +0 -0
- /package/dist/{utils → shared}/git.js +0 -0
- /package/dist/{utils → shared}/patterns.js +0 -0
- /package/dist/{utils → shared}/secret-store.js +0 -0
- /package/dist/{errors → shared}/vizzly-error.js +0 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { cosmiconfigSync } from 'cosmiconfig';
|
|
5
|
+
import { loadGlobalConfig, saveGlobalConfig } from '../../shared/global-config.js';
|
|
6
|
+
import * as output from '../../shared/output.js';
|
|
7
|
+
import { VizzlyError } from '../../shared/vizzly-error.js';
|
|
8
|
+
import { deepMerge, extractCosmiconfigResult, extractEnvOverrides } from './config.normalizers.js';
|
|
9
|
+
import { validateReadScope, validateWriteScope } from './config.rules.js';
|
|
10
|
+
import { buildGlobalConfigResult, buildMergedConfigResult, buildProjectConfigResult, getConfigFormat, serializeConfig } from './config.serializers.js';
|
|
11
|
+
import { CONFIG_DEFAULTS } from './config-defaults.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Config Operations - Configuration operations with dependency injection
|
|
15
|
+
*
|
|
16
|
+
* Each operation takes its dependencies as parameters:
|
|
17
|
+
* - explorer: cosmiconfig explorer for project config
|
|
18
|
+
* - globalConfigStore: for reading/writing global config
|
|
19
|
+
* - fileWriter: for writing project config files
|
|
20
|
+
*
|
|
21
|
+
* This makes them trivially testable without mocking modules.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Config Service
|
|
26
|
+
* Manages configuration for the TDD dashboard settings page
|
|
27
|
+
*
|
|
28
|
+
* Provides read/write access to:
|
|
29
|
+
* - Merged config (read-only combination of all sources)
|
|
30
|
+
* - Project config (vizzly.config.js in working directory)
|
|
31
|
+
* - Global config (~/.vizzly/config.json)
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a config service instance
|
|
36
|
+
* @param {Object} options
|
|
37
|
+
* @param {string} options.workingDir - Working directory for project config
|
|
38
|
+
* @returns {Object} Config service with getConfig, updateConfig, validateConfig methods
|
|
39
|
+
*/
|
|
40
|
+
export function createConfigService({
|
|
41
|
+
workingDir
|
|
42
|
+
}) {
|
|
43
|
+
let projectConfigPath = null;
|
|
44
|
+
let projectConfigFormat = 'js'; // 'js' or 'json'
|
|
45
|
+
|
|
46
|
+
// Find project config file
|
|
47
|
+
let explorer = cosmiconfigSync('vizzly');
|
|
48
|
+
let searchResult = explorer.search(workingDir);
|
|
49
|
+
if (searchResult?.filepath) {
|
|
50
|
+
projectConfigPath = searchResult.filepath;
|
|
51
|
+
projectConfigFormat = searchResult.filepath.endsWith('.json') ? 'json' : 'js';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get configuration by type
|
|
56
|
+
* @param {'merged'|'project'|'global'} type
|
|
57
|
+
* @returns {Promise<Object>}
|
|
58
|
+
*/
|
|
59
|
+
async function getConfig(type) {
|
|
60
|
+
if (type === 'merged') {
|
|
61
|
+
return getMergedConfig();
|
|
62
|
+
} else if (type === 'project') {
|
|
63
|
+
return getProjectConfig();
|
|
64
|
+
} else if (type === 'global') {
|
|
65
|
+
return getGlobalConfigData();
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`Unknown config type: ${type}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get merged configuration with source tracking
|
|
72
|
+
*/
|
|
73
|
+
async function getMergedConfig() {
|
|
74
|
+
let config = cloneConfigValue(CONFIG_DEFAULTS);
|
|
75
|
+
let sources = {};
|
|
76
|
+
|
|
77
|
+
// Layer 1: Global config
|
|
78
|
+
let globalConfig = await loadGlobalConfig();
|
|
79
|
+
if (globalConfig.settings) {
|
|
80
|
+
mergeWithTracking(config, globalConfig.settings, sources, 'global');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Layer 2: Project config
|
|
84
|
+
if (projectConfigPath && existsSync(projectConfigPath)) {
|
|
85
|
+
try {
|
|
86
|
+
let result = explorer.load(projectConfigPath);
|
|
87
|
+
let projectConfig = result?.config?.default || result?.config || {};
|
|
88
|
+
mergeWithTracking(config, projectConfig, sources, 'project');
|
|
89
|
+
} catch (error) {
|
|
90
|
+
output.debug('config-service', `Error loading project config: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Layer 3: Environment variables
|
|
95
|
+
if (process.env.VIZZLY_THRESHOLD) {
|
|
96
|
+
config.comparison.threshold = Number(process.env.VIZZLY_THRESHOLD);
|
|
97
|
+
sources.comparison = 'env';
|
|
98
|
+
}
|
|
99
|
+
if (process.env.VIZZLY_MIN_CLUSTER_SIZE) {
|
|
100
|
+
config.comparison.minClusterSize = Number(process.env.VIZZLY_MIN_CLUSTER_SIZE);
|
|
101
|
+
sources.comparison = 'env';
|
|
102
|
+
}
|
|
103
|
+
if (process.env.VIZZLY_PORT) {
|
|
104
|
+
config.server.port = Number(process.env.VIZZLY_PORT);
|
|
105
|
+
sources.server = 'env';
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
config,
|
|
109
|
+
sources
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get project-level configuration only
|
|
115
|
+
*/
|
|
116
|
+
async function getProjectConfig() {
|
|
117
|
+
if (!projectConfigPath || !existsSync(projectConfigPath)) {
|
|
118
|
+
return {
|
|
119
|
+
config: {},
|
|
120
|
+
path: null
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
let result = explorer.load(projectConfigPath);
|
|
125
|
+
let config = result?.config?.default || result?.config || {};
|
|
126
|
+
return {
|
|
127
|
+
config,
|
|
128
|
+
path: projectConfigPath
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
output.debug('config-service', `Error loading project config: ${error.message}`);
|
|
132
|
+
return {
|
|
133
|
+
config: {},
|
|
134
|
+
path: projectConfigPath,
|
|
135
|
+
error: error.message
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get global configuration only
|
|
142
|
+
*/
|
|
143
|
+
async function getGlobalConfigData() {
|
|
144
|
+
let globalConfig = await loadGlobalConfig();
|
|
145
|
+
return {
|
|
146
|
+
config: globalConfig.settings || {},
|
|
147
|
+
path: join(process.env.VIZZLY_HOME || join(process.env.HOME || '', '.vizzly'), 'config.json')
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Update configuration by type
|
|
153
|
+
* @param {'project'|'global'} type
|
|
154
|
+
* @param {Object} updates - Config updates to apply
|
|
155
|
+
* @returns {Promise<Object>}
|
|
156
|
+
*/
|
|
157
|
+
async function updateConfig(type, updates) {
|
|
158
|
+
if (type === 'project') {
|
|
159
|
+
return updateProjectConfig(updates);
|
|
160
|
+
} else if (type === 'global') {
|
|
161
|
+
return updateGlobalConfig(updates);
|
|
162
|
+
}
|
|
163
|
+
throw new Error(`Cannot update config type: ${type}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Update project configuration (vizzly.config.js)
|
|
168
|
+
*/
|
|
169
|
+
async function updateProjectConfig(updates) {
|
|
170
|
+
// If no project config exists, create one
|
|
171
|
+
if (!projectConfigPath) {
|
|
172
|
+
projectConfigPath = join(workingDir, 'vizzly.config.js');
|
|
173
|
+
projectConfigFormat = 'js';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Read existing config
|
|
177
|
+
let existingConfig = {};
|
|
178
|
+
if (existsSync(projectConfigPath)) {
|
|
179
|
+
try {
|
|
180
|
+
let result = explorer.load(projectConfigPath);
|
|
181
|
+
existingConfig = result?.config?.default || result?.config || {};
|
|
182
|
+
} catch {
|
|
183
|
+
// Start fresh if corrupted
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Merge updates
|
|
188
|
+
let newConfig = mergeDeep(existingConfig, updates);
|
|
189
|
+
|
|
190
|
+
// Write based on format
|
|
191
|
+
if (projectConfigFormat === 'json') {
|
|
192
|
+
await writeFile(projectConfigPath, JSON.stringify(newConfig, null, 2));
|
|
193
|
+
} else {
|
|
194
|
+
// Write as ES module
|
|
195
|
+
let content = `import { defineConfig } from '@vizzly-testing/cli/config';
|
|
196
|
+
|
|
197
|
+
export default defineConfig(${JSON.stringify(newConfig, null, 2)});
|
|
198
|
+
`;
|
|
199
|
+
await writeFile(projectConfigPath, content);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Clear cosmiconfig cache so next read gets fresh data
|
|
203
|
+
explorer.clearCaches();
|
|
204
|
+
return {
|
|
205
|
+
success: true,
|
|
206
|
+
path: projectConfigPath
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Update global configuration (~/.vizzly/config.json)
|
|
212
|
+
*/
|
|
213
|
+
async function updateGlobalConfig(updates) {
|
|
214
|
+
let globalConfig = await loadGlobalConfig();
|
|
215
|
+
if (!globalConfig.settings) {
|
|
216
|
+
globalConfig.settings = {};
|
|
217
|
+
}
|
|
218
|
+
globalConfig.settings = mergeDeep(globalConfig.settings, updates);
|
|
219
|
+
await saveGlobalConfig(globalConfig);
|
|
220
|
+
return {
|
|
221
|
+
success: true
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Validate configuration
|
|
227
|
+
* @param {Object} config - Config to validate
|
|
228
|
+
* @returns {Promise<Object>}
|
|
229
|
+
*/
|
|
230
|
+
async function validateConfig(config) {
|
|
231
|
+
let errors = [];
|
|
232
|
+
let warnings = [];
|
|
233
|
+
|
|
234
|
+
// Validate threshold
|
|
235
|
+
if (config.comparison?.threshold !== undefined) {
|
|
236
|
+
let threshold = config.comparison.threshold;
|
|
237
|
+
if (typeof threshold !== 'number' || !Number.isFinite(threshold) || threshold < 0) {
|
|
238
|
+
errors.push('comparison.threshold must be a non-negative number');
|
|
239
|
+
} else if (threshold > 100) {
|
|
240
|
+
warnings.push('comparison.threshold above 100 may cause all comparisons to pass');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Validate minClusterSize
|
|
245
|
+
if (config.comparison?.minClusterSize !== undefined) {
|
|
246
|
+
let minClusterSize = config.comparison.minClusterSize;
|
|
247
|
+
if (!Number.isInteger(minClusterSize) || minClusterSize < 1) {
|
|
248
|
+
errors.push('comparison.minClusterSize must be a positive integer (1 or greater)');
|
|
249
|
+
} else if (minClusterSize > 100) {
|
|
250
|
+
warnings.push('comparison.minClusterSize above 100 may filter out most differences');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Validate port
|
|
255
|
+
if (config.server?.port !== undefined) {
|
|
256
|
+
let port = config.server.port;
|
|
257
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
258
|
+
errors.push('server.port must be an integer between 1 and 65535');
|
|
259
|
+
} else if (port < 1024) {
|
|
260
|
+
warnings.push('server.port below 1024 may require elevated privileges');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Validate timeout
|
|
265
|
+
if (config.server?.timeout !== undefined) {
|
|
266
|
+
let timeout = config.server.timeout;
|
|
267
|
+
if (!Number.isInteger(timeout) || timeout < 0) {
|
|
268
|
+
errors.push('server.timeout must be a non-negative integer');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
valid: errors.length === 0,
|
|
273
|
+
errors,
|
|
274
|
+
warnings
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
getConfig,
|
|
279
|
+
updateConfig,
|
|
280
|
+
validateConfig
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Deep merge two objects
|
|
286
|
+
*/
|
|
287
|
+
function mergeDeep(target, source) {
|
|
288
|
+
let result = cloneConfigValue(target);
|
|
289
|
+
for (let key in source) {
|
|
290
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
291
|
+
result[key] = mergeDeep(result[key] || {}, source[key]);
|
|
292
|
+
} else if (Array.isArray(source[key])) {
|
|
293
|
+
result[key] = [...source[key]];
|
|
294
|
+
} else {
|
|
295
|
+
result[key] = source[key];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
function cloneConfigValue(value) {
|
|
301
|
+
if (Array.isArray(value)) {
|
|
302
|
+
return value.map(cloneConfigValue);
|
|
303
|
+
}
|
|
304
|
+
if (value && typeof value === 'object') {
|
|
305
|
+
let clone = {};
|
|
306
|
+
for (let key in value) {
|
|
307
|
+
clone[key] = cloneConfigValue(value[key]);
|
|
308
|
+
}
|
|
309
|
+
return clone;
|
|
310
|
+
}
|
|
311
|
+
return value;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Merge config with source tracking
|
|
316
|
+
*/
|
|
317
|
+
function mergeWithTracking(target, source, sources, sourceName, sectionName) {
|
|
318
|
+
for (let key in source) {
|
|
319
|
+
let sourceSection = sectionName || key;
|
|
320
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
321
|
+
if (!target[key]) target[key] = {};
|
|
322
|
+
mergeWithTracking(target[key], source[key], sources, sourceName, sourceSection);
|
|
323
|
+
} else {
|
|
324
|
+
target[key] = source[key];
|
|
325
|
+
sources[sourceSection] = sourceName;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get project-level config from vizzly.config.js or similar
|
|
332
|
+
* @param {Object} explorer - Cosmiconfig explorer with search method
|
|
333
|
+
* @param {string} projectRoot - Project root directory
|
|
334
|
+
* @returns {Promise<{ config: Object, filepath: string|null, isEmpty: boolean }>}
|
|
335
|
+
*/
|
|
336
|
+
export async function getProjectConfig(explorer, projectRoot) {
|
|
337
|
+
let result = explorer.search(projectRoot);
|
|
338
|
+
let {
|
|
339
|
+
config,
|
|
340
|
+
filepath
|
|
341
|
+
} = extractCosmiconfigResult(result);
|
|
342
|
+
return buildProjectConfigResult(config, filepath);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get global config from ~/.vizzly/config.json
|
|
347
|
+
* @param {Object} globalConfigStore - Store with load and getPath methods
|
|
348
|
+
* @returns {Promise<{ config: Object, filepath: string, isEmpty: boolean }>}
|
|
349
|
+
*/
|
|
350
|
+
export async function getGlobalConfig(globalConfigStore) {
|
|
351
|
+
let config = await globalConfigStore.load();
|
|
352
|
+
let filepath = globalConfigStore.getPath();
|
|
353
|
+
return buildGlobalConfigResult(config, filepath);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get merged config from all sources with source tracking
|
|
358
|
+
* @param {Object} options - Options
|
|
359
|
+
* @param {Object} options.explorer - Cosmiconfig explorer
|
|
360
|
+
* @param {Object} options.globalConfigStore - Global config store
|
|
361
|
+
* @param {string} options.projectRoot - Project root directory
|
|
362
|
+
* @param {Object} [options.env] - Environment variables (defaults to process.env)
|
|
363
|
+
* @returns {Promise<{ config: Object, sources: Object, projectFilepath: string|null, globalFilepath: string }>}
|
|
364
|
+
*/
|
|
365
|
+
export async function getMergedConfig({
|
|
366
|
+
explorer,
|
|
367
|
+
globalConfigStore,
|
|
368
|
+
projectRoot,
|
|
369
|
+
env = process.env
|
|
370
|
+
}) {
|
|
371
|
+
let projectConfigData = await getProjectConfig(explorer, projectRoot);
|
|
372
|
+
let globalConfigData = await getGlobalConfig(globalConfigStore);
|
|
373
|
+
let envOverrides = extractEnvOverrides(env);
|
|
374
|
+
return buildMergedConfigResult({
|
|
375
|
+
projectConfig: projectConfigData.config,
|
|
376
|
+
globalConfig: globalConfigData.config,
|
|
377
|
+
envOverrides,
|
|
378
|
+
projectFilepath: projectConfigData.filepath,
|
|
379
|
+
globalFilepath: globalConfigData.filepath
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get configuration based on scope
|
|
385
|
+
* @param {Object} options - Options
|
|
386
|
+
* @param {string} options.scope - 'project', 'global', or 'merged'
|
|
387
|
+
* @param {Object} options.explorer - Cosmiconfig explorer
|
|
388
|
+
* @param {Object} options.globalConfigStore - Global config store
|
|
389
|
+
* @param {string} options.projectRoot - Project root directory
|
|
390
|
+
* @param {Object} [options.env] - Environment variables
|
|
391
|
+
* @returns {Promise<Object>} Config result based on scope
|
|
392
|
+
*/
|
|
393
|
+
export async function getConfig({
|
|
394
|
+
scope = 'merged',
|
|
395
|
+
explorer,
|
|
396
|
+
globalConfigStore,
|
|
397
|
+
projectRoot,
|
|
398
|
+
env
|
|
399
|
+
}) {
|
|
400
|
+
let validation = validateReadScope(scope);
|
|
401
|
+
if (!validation.valid) {
|
|
402
|
+
throw validation.error;
|
|
403
|
+
}
|
|
404
|
+
if (scope === 'project') {
|
|
405
|
+
return getProjectConfig(explorer, projectRoot);
|
|
406
|
+
}
|
|
407
|
+
if (scope === 'global') {
|
|
408
|
+
return getGlobalConfig(globalConfigStore);
|
|
409
|
+
}
|
|
410
|
+
return getMergedConfig({
|
|
411
|
+
explorer,
|
|
412
|
+
globalConfigStore,
|
|
413
|
+
projectRoot,
|
|
414
|
+
env
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Update project-level config
|
|
420
|
+
* @param {Object} options - Options
|
|
421
|
+
* @param {Object} options.updates - Config updates to apply
|
|
422
|
+
* @param {Object} options.explorer - Cosmiconfig explorer
|
|
423
|
+
* @param {string} options.projectRoot - Project root directory
|
|
424
|
+
* @param {Function} options.writeFile - Async file writer (path, content) => Promise
|
|
425
|
+
* @param {Function} options.readFile - Async file reader (path) => Promise<string>
|
|
426
|
+
* @param {Function} options.validate - Config validator
|
|
427
|
+
* @returns {Promise<{ config: Object, filepath: string }>}
|
|
428
|
+
*/
|
|
429
|
+
export async function updateProjectConfig({
|
|
430
|
+
updates,
|
|
431
|
+
explorer,
|
|
432
|
+
projectRoot,
|
|
433
|
+
writeFile,
|
|
434
|
+
readFile,
|
|
435
|
+
validate
|
|
436
|
+
}) {
|
|
437
|
+
let result = explorer.search(projectRoot);
|
|
438
|
+
let {
|
|
439
|
+
config: currentConfig,
|
|
440
|
+
filepath: configPath
|
|
441
|
+
} = extractCosmiconfigResult(result);
|
|
442
|
+
|
|
443
|
+
// Determine config file path - create new if none exists
|
|
444
|
+
if (!configPath) {
|
|
445
|
+
configPath = join(projectRoot, 'vizzly.config.js');
|
|
446
|
+
currentConfig = {};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Merge updates with current config
|
|
450
|
+
let newConfig = deepMerge(currentConfig, updates);
|
|
451
|
+
|
|
452
|
+
// Validate before writing
|
|
453
|
+
try {
|
|
454
|
+
validate(newConfig);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
throw new VizzlyError(`Invalid configuration: ${error.message}`, 'CONFIG_VALIDATION_ERROR', {
|
|
457
|
+
errors: error.errors
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Write config file based on format
|
|
462
|
+
await writeProjectConfigFile({
|
|
463
|
+
filepath: configPath,
|
|
464
|
+
config: newConfig,
|
|
465
|
+
writeFile,
|
|
466
|
+
readFile
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Clear cosmiconfig cache
|
|
470
|
+
explorer.clearCaches();
|
|
471
|
+
return {
|
|
472
|
+
config: newConfig,
|
|
473
|
+
filepath: configPath
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Write project config to file
|
|
479
|
+
* @param {Object} options - Options
|
|
480
|
+
* @param {string} options.filepath - Path to write to
|
|
481
|
+
* @param {Object} options.config - Config object
|
|
482
|
+
* @param {Function} options.writeFile - Async file writer
|
|
483
|
+
* @param {Function} options.readFile - Async file reader (for package.json)
|
|
484
|
+
* @returns {Promise<void>}
|
|
485
|
+
*/
|
|
486
|
+
export async function writeProjectConfigFile({
|
|
487
|
+
filepath,
|
|
488
|
+
config,
|
|
489
|
+
writeFile,
|
|
490
|
+
readFile
|
|
491
|
+
}) {
|
|
492
|
+
let format = getConfigFormat(filepath);
|
|
493
|
+
if (format === 'package') {
|
|
494
|
+
// For package.json, merge into existing
|
|
495
|
+
let pkgContent = await readFile(filepath);
|
|
496
|
+
let pkg;
|
|
497
|
+
try {
|
|
498
|
+
pkg = JSON.parse(pkgContent);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
throw new VizzlyError(`Failed to parse package.json: ${error.message}`, 'INVALID_PACKAGE_JSON');
|
|
501
|
+
}
|
|
502
|
+
pkg.vizzly = config;
|
|
503
|
+
await writeFile(filepath, JSON.stringify(pkg, null, 2));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
let serialized = serializeConfig(config, filepath);
|
|
507
|
+
if (serialized.error) {
|
|
508
|
+
throw serialized.error;
|
|
509
|
+
}
|
|
510
|
+
await writeFile(filepath, serialized.content);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Update global config
|
|
515
|
+
* @param {Object} options - Options
|
|
516
|
+
* @param {Object} options.updates - Config updates to apply
|
|
517
|
+
* @param {Object} options.globalConfigStore - Global config store with load and save methods
|
|
518
|
+
* @returns {Promise<{ config: Object, filepath: string }>}
|
|
519
|
+
*/
|
|
520
|
+
export async function updateGlobalConfig({
|
|
521
|
+
updates,
|
|
522
|
+
globalConfigStore
|
|
523
|
+
}) {
|
|
524
|
+
let currentConfig = await globalConfigStore.load();
|
|
525
|
+
let newConfig = deepMerge(currentConfig, updates);
|
|
526
|
+
await globalConfigStore.save(newConfig);
|
|
527
|
+
return {
|
|
528
|
+
config: newConfig,
|
|
529
|
+
filepath: globalConfigStore.getPath()
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Update configuration based on scope
|
|
535
|
+
* @param {Object} options - Options
|
|
536
|
+
* @param {string} options.scope - 'project' or 'global'
|
|
537
|
+
* @param {Object} options.updates - Config updates to apply
|
|
538
|
+
* @param {Object} options.explorer - Cosmiconfig explorer
|
|
539
|
+
* @param {Object} options.globalConfigStore - Global config store
|
|
540
|
+
* @param {string} options.projectRoot - Project root directory
|
|
541
|
+
* @param {Function} options.writeFile - Async file writer
|
|
542
|
+
* @param {Function} options.readFile - Async file reader
|
|
543
|
+
* @param {Function} options.validate - Config validator
|
|
544
|
+
* @returns {Promise<{ config: Object, filepath: string }>}
|
|
545
|
+
*/
|
|
546
|
+
export async function updateConfig({
|
|
547
|
+
scope,
|
|
548
|
+
updates,
|
|
549
|
+
explorer,
|
|
550
|
+
globalConfigStore,
|
|
551
|
+
projectRoot,
|
|
552
|
+
writeFile,
|
|
553
|
+
readFile,
|
|
554
|
+
validate
|
|
555
|
+
}) {
|
|
556
|
+
let validation = validateWriteScope(scope);
|
|
557
|
+
if (!validation.valid) {
|
|
558
|
+
throw validation.error;
|
|
559
|
+
}
|
|
560
|
+
if (scope === 'project') {
|
|
561
|
+
return updateProjectConfig({
|
|
562
|
+
updates,
|
|
563
|
+
explorer,
|
|
564
|
+
projectRoot,
|
|
565
|
+
writeFile,
|
|
566
|
+
readFile,
|
|
567
|
+
validate
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
return updateGlobalConfig({
|
|
571
|
+
updates,
|
|
572
|
+
globalConfigStore
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Validate configuration object
|
|
578
|
+
* @param {Object} config - Config to validate
|
|
579
|
+
* @param {Function} validateFn - Validation function
|
|
580
|
+
* @returns {{ valid: boolean, config: Object|null, errors: Array }}
|
|
581
|
+
*/
|
|
582
|
+
export function validateConfig(config, validateFn) {
|
|
583
|
+
try {
|
|
584
|
+
let validated = validateFn(config);
|
|
585
|
+
return {
|
|
586
|
+
valid: true,
|
|
587
|
+
config: validated,
|
|
588
|
+
errors: []
|
|
589
|
+
};
|
|
590
|
+
} catch (error) {
|
|
591
|
+
return {
|
|
592
|
+
valid: false,
|
|
593
|
+
config: null,
|
|
594
|
+
errors: error.errors || [{
|
|
595
|
+
message: error.message
|
|
596
|
+
}]
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Get the source of a specific config key
|
|
603
|
+
* @param {Object} options - Options
|
|
604
|
+
* @param {string} options.key - Config key to look up
|
|
605
|
+
* @param {Object} options.explorer - Cosmiconfig explorer
|
|
606
|
+
* @param {Object} options.globalConfigStore - Global config store
|
|
607
|
+
* @param {string} options.projectRoot - Project root directory
|
|
608
|
+
* @param {Object} [options.env] - Environment variables
|
|
609
|
+
* @returns {Promise<string>} Source ('default', 'global', 'project', 'env', 'unknown')
|
|
610
|
+
*/
|
|
611
|
+
export async function getConfigSource({
|
|
612
|
+
key,
|
|
613
|
+
explorer,
|
|
614
|
+
globalConfigStore,
|
|
615
|
+
projectRoot,
|
|
616
|
+
env
|
|
617
|
+
}) {
|
|
618
|
+
let merged = await getMergedConfig({
|
|
619
|
+
explorer,
|
|
620
|
+
globalConfigStore,
|
|
621
|
+
projectRoot,
|
|
622
|
+
env
|
|
623
|
+
});
|
|
624
|
+
return merged.sources[key] || 'unknown';
|
|
625
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CONFIG_DEFAULTS } from './config-defaults.js';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Configuration schema validation for Vizzly CLI
|
|
3
6
|
* Uses Zod for runtime validation
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
|
-
import { z } from 'zod';
|
|
7
|
-
import { CONFIG_DEFAULTS } from '../config/core.js';
|
|
8
|
-
|
|
9
9
|
/**
|
|
10
10
|
* Server configuration schema
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
let serverSchema = z.object({
|
|
13
13
|
port: z.number().int().positive().default(CONFIG_DEFAULTS.server.port),
|
|
14
14
|
timeout: z.number().int().positive().default(CONFIG_DEFAULTS.server.timeout)
|
|
15
15
|
});
|
|
@@ -17,7 +17,7 @@ const serverSchema = z.object({
|
|
|
17
17
|
/**
|
|
18
18
|
* Build configuration schema
|
|
19
19
|
*/
|
|
20
|
-
|
|
20
|
+
let buildSchema = z.object({
|
|
21
21
|
name: z.string().default(CONFIG_DEFAULTS.build.name),
|
|
22
22
|
environment: z.string().default(CONFIG_DEFAULTS.build.environment),
|
|
23
23
|
branch: z.string().optional(),
|
|
@@ -28,7 +28,7 @@ const buildSchema = z.object({
|
|
|
28
28
|
/**
|
|
29
29
|
* Upload configuration schema
|
|
30
30
|
*/
|
|
31
|
-
|
|
31
|
+
let uploadSchema = z.object({
|
|
32
32
|
screenshotsDir: z.union([z.string(), z.array(z.string())]).default(CONFIG_DEFAULTS.upload.screenshotsDir),
|
|
33
33
|
batchSize: z.number().int().positive().default(CONFIG_DEFAULTS.upload.batchSize),
|
|
34
34
|
timeout: z.number().int().positive().default(CONFIG_DEFAULTS.upload.timeout)
|
|
@@ -39,7 +39,7 @@ const uploadSchema = z.object({
|
|
|
39
39
|
* threshold: CIEDE2000 Delta E units (0.0 = exact, 1.0 = JND, 2.0 = recommended, 3.0+ = permissive)
|
|
40
40
|
* minClusterSize: pixels (1 = exact)
|
|
41
41
|
*/
|
|
42
|
-
|
|
42
|
+
let comparisonSchema = z.object({
|
|
43
43
|
threshold: z.number().min(0).default(CONFIG_DEFAULTS.comparison.threshold),
|
|
44
44
|
minClusterSize: z.int().min(1).default(CONFIG_DEFAULTS.comparison.minClusterSize)
|
|
45
45
|
});
|
|
@@ -47,7 +47,7 @@ const comparisonSchema = z.object({
|
|
|
47
47
|
/**
|
|
48
48
|
* TDD configuration schema
|
|
49
49
|
*/
|
|
50
|
-
|
|
50
|
+
let tddSchema = z.object({
|
|
51
51
|
openReport: z.boolean().default(CONFIG_DEFAULTS.tdd.openReport)
|
|
52
52
|
});
|
|
53
53
|
let projectSchema = z.object({
|
|
@@ -59,7 +59,7 @@ let projectSchema = z.object({
|
|
|
59
59
|
* Core Vizzly configuration schema
|
|
60
60
|
* Allows plugin-specific keys with passthrough for extensibility
|
|
61
61
|
*/
|
|
62
|
-
export
|
|
62
|
+
export let vizzlyConfigSchema = z.object({
|
|
63
63
|
// Core Vizzly config
|
|
64
64
|
apiKey: z.string().optional(),
|
|
65
65
|
apiUrl: z.string().url().default(CONFIG_DEFAULTS.apiUrl),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getLogLevel } from '../../shared/log-level.js';
|
|
1
2
|
/**
|
|
2
3
|
* Environment Configuration Utility
|
|
3
4
|
* Centralized access to environment variables with proper defaults
|
|
@@ -29,14 +30,6 @@ export function getApiUrl() {
|
|
|
29
30
|
return process.env.VIZZLY_API_URL || 'https://app.vizzly.dev';
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
/**
|
|
33
|
-
* Get log level from environment
|
|
34
|
-
* @returns {string} Log level with default
|
|
35
|
-
*/
|
|
36
|
-
export function getLogLevel() {
|
|
37
|
-
return process.env.VIZZLY_LOG_LEVEL || 'info';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
33
|
/**
|
|
41
34
|
* Get user agent from environment
|
|
42
35
|
* @returns {string|undefined} User agent string
|