@vizzly-testing/cli 0.20.0 → 0.20.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) 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 +1 -1
  10. package/dist/commands/doctor.js +3 -3
  11. package/dist/commands/finalize.js +41 -15
  12. package/dist/commands/login.js +7 -6
  13. package/dist/commands/logout.js +4 -4
  14. package/dist/commands/project.js +5 -4
  15. package/dist/commands/run.js +158 -90
  16. package/dist/commands/status.js +22 -18
  17. package/dist/commands/tdd.js +105 -78
  18. package/dist/commands/upload.js +61 -26
  19. package/dist/commands/whoami.js +4 -4
  20. package/dist/config/core.js +438 -0
  21. package/dist/config/index.js +13 -0
  22. package/dist/config/operations.js +327 -0
  23. package/dist/index.js +1 -1
  24. package/dist/project/core.js +295 -0
  25. package/dist/project/index.js +13 -0
  26. package/dist/project/operations.js +393 -0
  27. package/dist/report-generator/core.js +315 -0
  28. package/dist/report-generator/index.js +8 -0
  29. package/dist/report-generator/operations.js +196 -0
  30. package/dist/reporter/reporter-bundle.iife.js +16 -16
  31. package/dist/screenshot-server/core.js +157 -0
  32. package/dist/screenshot-server/index.js +11 -0
  33. package/dist/screenshot-server/operations.js +183 -0
  34. package/dist/sdk/index.js +3 -2
  35. package/dist/server/handlers/api-handler.js +14 -5
  36. package/dist/server/handlers/tdd-handler.js +80 -48
  37. package/dist/server-manager/core.js +183 -0
  38. package/dist/server-manager/index.js +81 -0
  39. package/dist/server-manager/operations.js +208 -0
  40. package/dist/services/build-manager.js +2 -69
  41. package/dist/services/index.js +21 -48
  42. package/dist/services/screenshot-server.js +40 -74
  43. package/dist/services/server-manager.js +45 -80
  44. package/dist/services/static-report-generator.js +21 -163
  45. package/dist/services/test-runner.js +90 -250
  46. package/dist/services/uploader.js +56 -358
  47. package/dist/tdd/core/hotspot-coverage.js +112 -0
  48. package/dist/tdd/core/signature.js +101 -0
  49. package/dist/tdd/index.js +19 -0
  50. package/dist/tdd/metadata/baseline-metadata.js +103 -0
  51. package/dist/tdd/metadata/hotspot-metadata.js +93 -0
  52. package/dist/tdd/services/baseline-downloader.js +151 -0
  53. package/dist/tdd/services/baseline-manager.js +166 -0
  54. package/dist/tdd/services/comparison-service.js +230 -0
  55. package/dist/tdd/services/hotspot-service.js +71 -0
  56. package/dist/tdd/services/result-service.js +123 -0
  57. package/dist/tdd/tdd-service.js +1081 -0
  58. package/dist/test-runner/core.js +255 -0
  59. package/dist/test-runner/index.js +13 -0
  60. package/dist/test-runner/operations.js +483 -0
  61. package/dist/uploader/core.js +396 -0
  62. package/dist/uploader/index.js +11 -0
  63. package/dist/uploader/operations.js +412 -0
  64. package/package.json +7 -12
  65. package/dist/services/api-service.js +0 -412
  66. package/dist/services/auth-service.js +0 -226
  67. package/dist/services/config-service.js +0 -369
  68. package/dist/services/html-report-generator.js +0 -455
  69. package/dist/services/project-service.js +0 -326
  70. package/dist/services/report-generator/report.css +0 -411
  71. package/dist/services/report-generator/viewer.js +0 -102
  72. package/dist/services/tdd-service.js +0 -1437
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Config Operations - Configuration operations with dependency injection
3
+ *
4
+ * Each operation takes its dependencies as parameters:
5
+ * - explorer: cosmiconfig explorer for project config
6
+ * - globalConfigStore: for reading/writing global config
7
+ * - fileWriter: for writing project config files
8
+ *
9
+ * This makes them trivially testable without mocking modules.
10
+ */
11
+
12
+ import { join } from 'node:path';
13
+ import { VizzlyError } from '../errors/vizzly-error.js';
14
+ import { buildGlobalConfigResult, buildMergedConfigResult, buildProjectConfigResult, deepMerge, extractCosmiconfigResult, extractEnvOverrides, getConfigFormat, serializeConfig, validateReadScope, validateWriteScope } from './core.js';
15
+
16
+ // ============================================================================
17
+ // Read Operations
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Get project-level config from vizzly.config.js or similar
22
+ * @param {Object} explorer - Cosmiconfig explorer with search method
23
+ * @param {string} projectRoot - Project root directory
24
+ * @returns {Promise<{ config: Object, filepath: string|null, isEmpty: boolean }>}
25
+ */
26
+ export async function getProjectConfig(explorer, projectRoot) {
27
+ let result = explorer.search(projectRoot);
28
+ let {
29
+ config,
30
+ filepath
31
+ } = extractCosmiconfigResult(result);
32
+ return buildProjectConfigResult(config, filepath);
33
+ }
34
+
35
+ /**
36
+ * Get global config from ~/.vizzly/config.json
37
+ * @param {Object} globalConfigStore - Store with load and getPath methods
38
+ * @returns {Promise<{ config: Object, filepath: string, isEmpty: boolean }>}
39
+ */
40
+ export async function getGlobalConfig(globalConfigStore) {
41
+ let config = await globalConfigStore.load();
42
+ let filepath = globalConfigStore.getPath();
43
+ return buildGlobalConfigResult(config, filepath);
44
+ }
45
+
46
+ /**
47
+ * Get merged config from all sources with source tracking
48
+ * @param {Object} options - Options
49
+ * @param {Object} options.explorer - Cosmiconfig explorer
50
+ * @param {Object} options.globalConfigStore - Global config store
51
+ * @param {string} options.projectRoot - Project root directory
52
+ * @param {Object} [options.env] - Environment variables (defaults to process.env)
53
+ * @returns {Promise<{ config: Object, sources: Object, projectFilepath: string|null, globalFilepath: string }>}
54
+ */
55
+ export async function getMergedConfig({
56
+ explorer,
57
+ globalConfigStore,
58
+ projectRoot,
59
+ env = process.env
60
+ }) {
61
+ let projectConfigData = await getProjectConfig(explorer, projectRoot);
62
+ let globalConfigData = await getGlobalConfig(globalConfigStore);
63
+ let envOverrides = extractEnvOverrides(env);
64
+ return buildMergedConfigResult({
65
+ projectConfig: projectConfigData.config,
66
+ globalConfig: globalConfigData.config,
67
+ envOverrides,
68
+ projectFilepath: projectConfigData.filepath,
69
+ globalFilepath: globalConfigData.filepath
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Get configuration based on scope
75
+ * @param {Object} options - Options
76
+ * @param {string} options.scope - 'project', 'global', or 'merged'
77
+ * @param {Object} options.explorer - Cosmiconfig explorer
78
+ * @param {Object} options.globalConfigStore - Global config store
79
+ * @param {string} options.projectRoot - Project root directory
80
+ * @param {Object} [options.env] - Environment variables
81
+ * @returns {Promise<Object>} Config result based on scope
82
+ */
83
+ export async function getConfig({
84
+ scope = 'merged',
85
+ explorer,
86
+ globalConfigStore,
87
+ projectRoot,
88
+ env
89
+ }) {
90
+ let validation = validateReadScope(scope);
91
+ if (!validation.valid) {
92
+ throw validation.error;
93
+ }
94
+ if (scope === 'project') {
95
+ return getProjectConfig(explorer, projectRoot);
96
+ }
97
+ if (scope === 'global') {
98
+ return getGlobalConfig(globalConfigStore);
99
+ }
100
+ return getMergedConfig({
101
+ explorer,
102
+ globalConfigStore,
103
+ projectRoot,
104
+ env
105
+ });
106
+ }
107
+
108
+ // ============================================================================
109
+ // Write Operations
110
+ // ============================================================================
111
+
112
+ /**
113
+ * Update project-level config
114
+ * @param {Object} options - Options
115
+ * @param {Object} options.updates - Config updates to apply
116
+ * @param {Object} options.explorer - Cosmiconfig explorer
117
+ * @param {string} options.projectRoot - Project root directory
118
+ * @param {Function} options.writeFile - Async file writer (path, content) => Promise
119
+ * @param {Function} options.readFile - Async file reader (path) => Promise<string>
120
+ * @param {Function} options.validate - Config validator
121
+ * @returns {Promise<{ config: Object, filepath: string }>}
122
+ */
123
+ export async function updateProjectConfig({
124
+ updates,
125
+ explorer,
126
+ projectRoot,
127
+ writeFile,
128
+ readFile,
129
+ validate
130
+ }) {
131
+ let result = explorer.search(projectRoot);
132
+ let {
133
+ config: currentConfig,
134
+ filepath: configPath
135
+ } = extractCosmiconfigResult(result);
136
+
137
+ // Determine config file path - create new if none exists
138
+ if (!configPath) {
139
+ configPath = join(projectRoot, 'vizzly.config.js');
140
+ currentConfig = {};
141
+ }
142
+
143
+ // Merge updates with current config
144
+ let newConfig = deepMerge(currentConfig, updates);
145
+
146
+ // Validate before writing
147
+ try {
148
+ validate(newConfig);
149
+ } catch (error) {
150
+ throw new VizzlyError(`Invalid configuration: ${error.message}`, 'CONFIG_VALIDATION_ERROR', {
151
+ errors: error.errors
152
+ });
153
+ }
154
+
155
+ // Write config file based on format
156
+ await writeProjectConfigFile({
157
+ filepath: configPath,
158
+ config: newConfig,
159
+ writeFile,
160
+ readFile
161
+ });
162
+
163
+ // Clear cosmiconfig cache
164
+ explorer.clearCaches();
165
+ return {
166
+ config: newConfig,
167
+ filepath: configPath
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Write project config to file
173
+ * @param {Object} options - Options
174
+ * @param {string} options.filepath - Path to write to
175
+ * @param {Object} options.config - Config object
176
+ * @param {Function} options.writeFile - Async file writer
177
+ * @param {Function} options.readFile - Async file reader (for package.json)
178
+ * @returns {Promise<void>}
179
+ */
180
+ export async function writeProjectConfigFile({
181
+ filepath,
182
+ config,
183
+ writeFile,
184
+ readFile
185
+ }) {
186
+ let format = getConfigFormat(filepath);
187
+ if (format === 'package') {
188
+ // For package.json, merge into existing
189
+ let pkgContent = await readFile(filepath);
190
+ let pkg;
191
+ try {
192
+ pkg = JSON.parse(pkgContent);
193
+ } catch (error) {
194
+ throw new VizzlyError(`Failed to parse package.json: ${error.message}`, 'INVALID_PACKAGE_JSON');
195
+ }
196
+ pkg.vizzly = config;
197
+ await writeFile(filepath, JSON.stringify(pkg, null, 2));
198
+ return;
199
+ }
200
+ let serialized = serializeConfig(config, filepath);
201
+ if (serialized.error) {
202
+ throw serialized.error;
203
+ }
204
+ await writeFile(filepath, serialized.content);
205
+ }
206
+
207
+ /**
208
+ * Update global config
209
+ * @param {Object} options - Options
210
+ * @param {Object} options.updates - Config updates to apply
211
+ * @param {Object} options.globalConfigStore - Global config store with load and save methods
212
+ * @returns {Promise<{ config: Object, filepath: string }>}
213
+ */
214
+ export async function updateGlobalConfig({
215
+ updates,
216
+ globalConfigStore
217
+ }) {
218
+ let currentConfig = await globalConfigStore.load();
219
+ let newConfig = deepMerge(currentConfig, updates);
220
+ await globalConfigStore.save(newConfig);
221
+ return {
222
+ config: newConfig,
223
+ filepath: globalConfigStore.getPath()
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Update configuration based on scope
229
+ * @param {Object} options - Options
230
+ * @param {string} options.scope - 'project' or 'global'
231
+ * @param {Object} options.updates - Config updates to apply
232
+ * @param {Object} options.explorer - Cosmiconfig explorer
233
+ * @param {Object} options.globalConfigStore - Global config store
234
+ * @param {string} options.projectRoot - Project root directory
235
+ * @param {Function} options.writeFile - Async file writer
236
+ * @param {Function} options.readFile - Async file reader
237
+ * @param {Function} options.validate - Config validator
238
+ * @returns {Promise<{ config: Object, filepath: string }>}
239
+ */
240
+ export async function updateConfig({
241
+ scope,
242
+ updates,
243
+ explorer,
244
+ globalConfigStore,
245
+ projectRoot,
246
+ writeFile,
247
+ readFile,
248
+ validate
249
+ }) {
250
+ let validation = validateWriteScope(scope);
251
+ if (!validation.valid) {
252
+ throw validation.error;
253
+ }
254
+ if (scope === 'project') {
255
+ return updateProjectConfig({
256
+ updates,
257
+ explorer,
258
+ projectRoot,
259
+ writeFile,
260
+ readFile,
261
+ validate
262
+ });
263
+ }
264
+ return updateGlobalConfig({
265
+ updates,
266
+ globalConfigStore
267
+ });
268
+ }
269
+
270
+ // ============================================================================
271
+ // Validation Operations
272
+ // ============================================================================
273
+
274
+ /**
275
+ * Validate configuration object
276
+ * @param {Object} config - Config to validate
277
+ * @param {Function} validateFn - Validation function
278
+ * @returns {{ valid: boolean, config: Object|null, errors: Array }}
279
+ */
280
+ export function validateConfig(config, validateFn) {
281
+ try {
282
+ let validated = validateFn(config);
283
+ return {
284
+ valid: true,
285
+ config: validated,
286
+ errors: []
287
+ };
288
+ } catch (error) {
289
+ return {
290
+ valid: false,
291
+ config: null,
292
+ errors: error.errors || [{
293
+ message: error.message
294
+ }]
295
+ };
296
+ }
297
+ }
298
+
299
+ // ============================================================================
300
+ // Source Lookup
301
+ // ============================================================================
302
+
303
+ /**
304
+ * Get the source of a specific config key
305
+ * @param {Object} options - Options
306
+ * @param {string} options.key - Config key to look up
307
+ * @param {Object} options.explorer - Cosmiconfig explorer
308
+ * @param {Object} options.globalConfigStore - Global config store
309
+ * @param {string} options.projectRoot - Project root directory
310
+ * @param {Object} [options.env] - Environment variables
311
+ * @returns {Promise<string>} Source ('default', 'global', 'project', 'env', 'unknown')
312
+ */
313
+ export async function getConfigSource({
314
+ key,
315
+ explorer,
316
+ globalConfigStore,
317
+ projectRoot,
318
+ env
319
+ }) {
320
+ let merged = await getMergedConfig({
321
+ explorer,
322
+ globalConfigStore,
323
+ projectRoot,
324
+ env
325
+ });
326
+ return merged.sources[key] || 'unknown';
327
+ }
package/dist/index.js CHANGED
@@ -15,9 +15,9 @@ export { UploadError } from './errors/vizzly-error.js';
15
15
  // Primary SDK export
16
16
  export { createVizzly } from './sdk/index.js';
17
17
  export { createServices } from './services/index.js';
18
- export { createTDDService } from './services/tdd-service.js';
19
18
  // Core services (for advanced usage)
20
19
  export { createUploader } from './services/uploader.js';
20
+ export { createTDDService } from './tdd/tdd-service.js';
21
21
  // Configuration helper
22
22
  export { defineConfig } from './utils/config-helpers.js';
23
23
  // Utilities
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Project Core - Pure functions for project 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
+ // Validation
11
+ // ============================================================================
12
+
13
+ /**
14
+ * Validate that a directory path is provided
15
+ * @param {string} directory - Directory path to validate
16
+ * @returns {{ valid: boolean, error: Error|null }}
17
+ */
18
+ export function validateDirectory(directory) {
19
+ if (!directory) {
20
+ return {
21
+ valid: false,
22
+ error: new VizzlyError('Directory path is required', 'INVALID_DIRECTORY')
23
+ };
24
+ }
25
+ return {
26
+ valid: true,
27
+ error: null
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Validate project data for mapping creation
33
+ * @param {Object} projectData - Project data to validate
34
+ * @param {string} [projectData.projectSlug] - Project slug
35
+ * @param {string} [projectData.organizationSlug] - Organization slug
36
+ * @param {string} [projectData.token] - Project token
37
+ * @returns {{ valid: boolean, error: Error|null }}
38
+ */
39
+ export function validateProjectData(projectData) {
40
+ if (!projectData.projectSlug) {
41
+ return {
42
+ valid: false,
43
+ error: new VizzlyError('Project slug is required', 'INVALID_PROJECT_DATA')
44
+ };
45
+ }
46
+ if (!projectData.organizationSlug) {
47
+ return {
48
+ valid: false,
49
+ error: new VizzlyError('Organization slug is required', 'INVALID_PROJECT_DATA')
50
+ };
51
+ }
52
+ if (!projectData.token) {
53
+ return {
54
+ valid: false,
55
+ error: new VizzlyError('Project token is required', 'INVALID_PROJECT_DATA')
56
+ };
57
+ }
58
+ return {
59
+ valid: true,
60
+ error: null
61
+ };
62
+ }
63
+
64
+ // ============================================================================
65
+ // Mapping Transformations
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Convert mappings object to array with directory paths included
70
+ * @param {Object} mappings - Object with directory paths as keys
71
+ * @returns {Array<Object>} Array of mappings with directory property
72
+ */
73
+ export function mappingsToArray(mappings) {
74
+ return Object.entries(mappings).map(([directory, data]) => ({
75
+ directory,
76
+ ...data
77
+ }));
78
+ }
79
+
80
+ /**
81
+ * Build a mapping result object
82
+ * @param {string} directory - Directory path
83
+ * @param {Object} projectData - Project data
84
+ * @returns {Object} Mapping result with directory included
85
+ */
86
+ export function buildMappingResult(directory, projectData) {
87
+ return {
88
+ directory,
89
+ ...projectData
90
+ };
91
+ }
92
+
93
+ // ============================================================================
94
+ // API Request Helpers
95
+ // ============================================================================
96
+
97
+ /**
98
+ * Build query params for builds API request
99
+ * @param {Object} options - Query options
100
+ * @param {number} [options.limit] - Number of builds to fetch
101
+ * @param {string} [options.branch] - Filter by branch
102
+ * @returns {string} Query string (empty string if no params)
103
+ */
104
+ export function buildBuildsQueryParams(options = {}) {
105
+ let params = new globalThis.URLSearchParams();
106
+ if (options.limit) {
107
+ params.append('limit', String(options.limit));
108
+ }
109
+ if (options.branch) {
110
+ params.append('branch', options.branch);
111
+ }
112
+ let query = params.toString();
113
+ return query ? `?${query}` : '';
114
+ }
115
+
116
+ /**
117
+ * Build organization header object
118
+ * @param {string} organizationSlug - Organization slug
119
+ * @returns {Object} Headers object with X-Organization header
120
+ */
121
+ export function buildOrgHeader(organizationSlug) {
122
+ return {
123
+ 'X-Organization': organizationSlug
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Build API URL for project endpoint
129
+ * @param {string} projectSlug - Project slug
130
+ * @returns {string} API URL path
131
+ */
132
+ export function buildProjectUrl(projectSlug) {
133
+ return `/api/project/${projectSlug}`;
134
+ }
135
+
136
+ /**
137
+ * Build API URL for builds endpoint
138
+ * @param {string} projectSlug - Project slug
139
+ * @param {Object} options - Query options
140
+ * @returns {string} Full API URL path with query params
141
+ */
142
+ export function buildBuildsUrl(projectSlug, options = {}) {
143
+ let queryString = buildBuildsQueryParams(options);
144
+ return `/api/build/${projectSlug}${queryString}`;
145
+ }
146
+
147
+ /**
148
+ * Build API URL for project tokens endpoint
149
+ * @param {string} organizationSlug - Organization slug
150
+ * @param {string} projectSlug - Project slug
151
+ * @param {string} [tokenId] - Optional token ID for specific token operations
152
+ * @returns {string} API URL path
153
+ */
154
+ export function buildTokensUrl(organizationSlug, projectSlug, tokenId) {
155
+ let base = `/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/tokens`;
156
+ return tokenId ? `${base}/${tokenId}` : base;
157
+ }
158
+
159
+ // ============================================================================
160
+ // Response Extraction
161
+ // ============================================================================
162
+
163
+ /**
164
+ * Extract projects from API response
165
+ * @param {Object} response - API response
166
+ * @returns {Array} Array of projects
167
+ */
168
+ export function extractProjects(response) {
169
+ return response?.projects || [];
170
+ }
171
+
172
+ /**
173
+ * Extract project from API response
174
+ * @param {Object} response - API response
175
+ * @returns {Object} Project object
176
+ */
177
+ export function extractProject(response) {
178
+ return response?.project || response;
179
+ }
180
+
181
+ /**
182
+ * Extract builds from API response
183
+ * @param {Object} response - API response
184
+ * @returns {Array} Array of builds
185
+ */
186
+ export function extractBuilds(response) {
187
+ return response?.builds || [];
188
+ }
189
+
190
+ /**
191
+ * Extract token from API response
192
+ * @param {Object} response - API response
193
+ * @returns {Object} Token object
194
+ */
195
+ export function extractToken(response) {
196
+ return response?.token;
197
+ }
198
+
199
+ /**
200
+ * Extract tokens from API response
201
+ * @param {Object} response - API response
202
+ * @returns {Array} Array of tokens
203
+ */
204
+ export function extractTokens(response) {
205
+ return response?.tokens || [];
206
+ }
207
+
208
+ /**
209
+ * Enrich projects with organization info
210
+ * @param {Array} projects - Array of projects
211
+ * @param {Object} org - Organization object
212
+ * @param {string} org.slug - Organization slug
213
+ * @param {string} org.name - Organization name
214
+ * @returns {Array} Projects with organization info added
215
+ */
216
+ export function enrichProjectsWithOrg(projects, org) {
217
+ return projects.map(project => ({
218
+ ...project,
219
+ organizationSlug: org.slug,
220
+ organizationName: org.name
221
+ }));
222
+ }
223
+
224
+ /**
225
+ * Extract organizations from whoami response
226
+ * @param {Object} whoamiResponse - Whoami API response
227
+ * @returns {Array} Array of organizations
228
+ */
229
+ export function extractOrganizations(whoamiResponse) {
230
+ return whoamiResponse?.organizations || [];
231
+ }
232
+
233
+ // ============================================================================
234
+ // Error Building
235
+ // ============================================================================
236
+
237
+ /**
238
+ * Build error for project fetch failure
239
+ * @param {Error} originalError - Original error
240
+ * @returns {VizzlyError} Wrapped error
241
+ */
242
+ export function buildProjectFetchError(originalError) {
243
+ return new VizzlyError(`Failed to fetch project: ${originalError.message}`, 'PROJECT_FETCH_FAILED', {
244
+ originalError
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Build error for no authentication available
250
+ * @returns {VizzlyError} No auth error
251
+ */
252
+ export function buildNoAuthError() {
253
+ return new VizzlyError('No authentication available', 'NO_AUTH_SERVICE');
254
+ }
255
+
256
+ /**
257
+ * Build error for no API service available
258
+ * @returns {VizzlyError} No API service error
259
+ */
260
+ export function buildNoApiServiceError() {
261
+ return new VizzlyError('API service not available', 'NO_API_SERVICE');
262
+ }
263
+
264
+ /**
265
+ * Build error for token creation failure
266
+ * @param {Error} originalError - Original error
267
+ * @returns {VizzlyError} Wrapped error
268
+ */
269
+ export function buildTokenCreateError(originalError) {
270
+ return new VizzlyError(`Failed to create project token: ${originalError.message}`, 'TOKEN_CREATE_FAILED', {
271
+ originalError
272
+ });
273
+ }
274
+
275
+ /**
276
+ * Build error for token fetch failure
277
+ * @param {Error} originalError - Original error
278
+ * @returns {VizzlyError} Wrapped error
279
+ */
280
+ export function buildTokensFetchError(originalError) {
281
+ return new VizzlyError(`Failed to fetch project tokens: ${originalError.message}`, 'TOKENS_FETCH_FAILED', {
282
+ originalError
283
+ });
284
+ }
285
+
286
+ /**
287
+ * Build error for token revoke failure
288
+ * @param {Error} originalError - Original error
289
+ * @returns {VizzlyError} Wrapped error
290
+ */
291
+ export function buildTokenRevokeError(originalError) {
292
+ return new VizzlyError(`Failed to revoke project token: ${originalError.message}`, 'TOKEN_REVOKE_FAILED', {
293
+ originalError
294
+ });
295
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Project Module - Public exports
3
+ *
4
+ * Provides functional project management primitives:
5
+ * - core.js: Pure functions for validation, URL building, response extraction
6
+ * - operations.js: Project operations with dependency injection
7
+ */
8
+
9
+ // Core pure functions
10
+ export { buildBuildsQueryParams, buildBuildsUrl, buildMappingResult, buildNoApiServiceError, buildNoAuthError, buildOrgHeader, buildProjectFetchError, buildProjectUrl, buildTokenCreateError, buildTokenRevokeError, buildTokensFetchError, buildTokensUrl, enrichProjectsWithOrg, extractBuilds, extractOrganizations, extractProject, extractProjects, extractToken, extractTokens, mappingsToArray, validateDirectory, validateProjectData } from './core.js';
11
+
12
+ // Project operations (take dependencies as parameters)
13
+ export { createMapping, createProjectToken, getMapping, getProject, getProjectWithApiToken, getProjectWithOAuth, getRecentBuilds, getRecentBuildsWithApiToken, getRecentBuildsWithOAuth, listMappings, listProjects, listProjectsWithApiToken, listProjectsWithOAuth, listProjectTokens, removeMapping, revokeProjectToken, switchProject } from './operations.js';