UNPKG

@vizzly-testing/cli

Version:

Visual review platform for UI developers and designers

326 lines (304 loc) 10.2 kB
/** * Project Service * Manages project mappings and project-related operations */ import { VizzlyError } from '../errors/vizzly-error.js'; import { getProjectMappings, saveProjectMapping, deleteProjectMapping, getProjectMapping } from '../utils/global-config.js'; /** * ProjectService for managing project mappings and operations */ export class ProjectService { constructor(config, options = {}) { this.config = config; this.apiService = options.apiService; this.authService = options.authService; } /** * List all project mappings * @returns {Promise<Array>} Array of project mappings */ async listMappings() { let mappings = await getProjectMappings(); // Convert object to array with directory path included return Object.entries(mappings).map(([directory, data]) => ({ directory, ...data })); } /** * Get project mapping for a specific directory * @param {string} directory - Directory path * @returns {Promise<Object|null>} Project mapping or null */ async getMapping(directory) { return getProjectMapping(directory); } /** * Create or update project mapping * @param {string} directory - Directory path * @param {Object} projectData - Project data * @param {string} projectData.projectSlug - Project slug * @param {string} projectData.organizationSlug - Organization slug * @param {string} projectData.token - Project API token * @param {string} [projectData.projectName] - Optional project name * @returns {Promise<Object>} Created mapping */ async createMapping(directory, projectData) { if (!directory) { throw new VizzlyError('Directory path is required', 'INVALID_DIRECTORY'); } if (!projectData.projectSlug) { throw new VizzlyError('Project slug is required', 'INVALID_PROJECT_DATA'); } if (!projectData.organizationSlug) { throw new VizzlyError('Organization slug is required', 'INVALID_PROJECT_DATA'); } if (!projectData.token) { throw new VizzlyError('Project token is required', 'INVALID_PROJECT_DATA'); } await saveProjectMapping(directory, projectData); return { directory, ...projectData }; } /** * Remove project mapping * @param {string} directory - Directory path * @returns {Promise<void>} */ async removeMapping(directory) { if (!directory) { throw new VizzlyError('Directory path is required', 'INVALID_DIRECTORY'); } await deleteProjectMapping(directory); } /** * Switch project for current directory * @param {string} projectSlug - Project slug * @param {string} organizationSlug - Organization slug * @param {string} token - Project token * @returns {Promise<Object>} Updated mapping */ async switchProject(projectSlug, organizationSlug, token) { let currentDir = process.cwd(); return this.createMapping(currentDir, { projectSlug, organizationSlug, token }); } /** * List all projects from API * Uses OAuth authentication (authService) when available, falls back to API token * @returns {Promise<Array>} Array of projects with organization info */ async listProjects() { // Try OAuth-based request first (user login via device flow) if (this.authService) { try { // First get the user's organizations via whoami let whoami = await this.authService.authenticatedRequest('/api/auth/cli/whoami', { method: 'GET' }); let organizations = whoami.organizations || []; if (organizations.length === 0) { return []; } // Fetch projects for each organization let allProjects = []; for (let org of organizations) { try { let response = await this.authService.authenticatedRequest('/api/project', { method: 'GET', headers: { 'X-Organization': org.slug } }); // Add organization info to each project let projects = (response.projects || []).map(project => ({ ...project, organizationSlug: org.slug, organizationName: org.name })); allProjects.push(...projects); } catch { // Silently skip failed orgs } } return allProjects; } catch { // Fall back to API token } } // Fall back to API token-based request (tokens are org-scoped, so no org header needed) if (this.apiService) { try { let response = await this.apiService.request('/api/project', { method: 'GET' }); return response.projects || []; } catch { return []; } } // No authentication available return []; } /** * Get project details * @param {string} projectSlug - Project slug * @param {string} organizationSlug - Organization slug * @returns {Promise<Object>} Project details */ async getProject(projectSlug, organizationSlug) { // Try OAuth-based request first if (this.authService) { try { let response = await this.authService.authenticatedRequest(`/api/project/${projectSlug}`, { method: 'GET', headers: { 'X-Organization': organizationSlug } }); return response.project || response; } catch { // Fall back to API token } } // Fall back to API token if (this.apiService) { try { let response = await this.apiService.request(`/api/project/${projectSlug}`, { method: 'GET', headers: { 'X-Organization': organizationSlug } }); return response.project || response; } catch (error) { throw new VizzlyError(`Failed to fetch project: ${error.message}`, 'PROJECT_FETCH_FAILED', { originalError: error }); } } throw new VizzlyError('No authentication available', 'NO_AUTH_SERVICE'); } /** * Get recent builds for a project * Uses OAuth authentication (authService) when available, falls back to API token * @param {string} projectSlug - Project slug * @param {string} organizationSlug - Organization slug * @param {Object} options - Query options * @param {number} [options.limit=10] - Number of builds to fetch * @param {string} [options.branch] - Filter by branch * @returns {Promise<Array>} Array of builds */ async getRecentBuilds(projectSlug, organizationSlug, options = {}) { let queryParams = new globalThis.URLSearchParams(); if (options.limit) queryParams.append('limit', String(options.limit)); if (options.branch) queryParams.append('branch', options.branch); let query = queryParams.toString(); let url = `/api/build/${projectSlug}${query ? `?${query}` : ''}`; // Try OAuth-based request first (user login via device flow) if (this.authService) { try { let response = await this.authService.authenticatedRequest(url, { method: 'GET', headers: { 'X-Organization': organizationSlug } }); return response.builds || []; } catch { // Fall back to API token } } // Fall back to API token-based request if (this.apiService) { try { let response = await this.apiService.request(url, { method: 'GET', headers: { 'X-Organization': organizationSlug } }); return response.builds || []; } catch { return []; } } // No authentication available return []; } /** * Create a project token * @param {string} projectSlug - Project slug * @param {string} organizationSlug - Organization slug * @param {Object} tokenData - Token data * @param {string} tokenData.name - Token name * @param {string} [tokenData.description] - Token description * @returns {Promise<Object>} Created token */ async createProjectToken(projectSlug, organizationSlug, tokenData) { if (!this.apiService) { throw new VizzlyError('API service not available', 'NO_API_SERVICE'); } try { let response = await this.apiService.request(`/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/tokens`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(tokenData) }); return response.token; } catch (error) { throw new VizzlyError(`Failed to create project token: ${error.message}`, 'TOKEN_CREATE_FAILED', { originalError: error }); } } /** * List project tokens * @param {string} projectSlug - Project slug * @param {string} organizationSlug - Organization slug * @returns {Promise<Array>} Array of tokens */ async listProjectTokens(projectSlug, organizationSlug) { if (!this.apiService) { throw new VizzlyError('API service not available', 'NO_API_SERVICE'); } try { let response = await this.apiService.request(`/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/tokens`, { method: 'GET' }); return response.tokens || []; } catch (error) { throw new VizzlyError(`Failed to fetch project tokens: ${error.message}`, 'TOKENS_FETCH_FAILED', { originalError: error }); } } /** * Revoke a project token * @param {string} projectSlug - Project slug * @param {string} organizationSlug - Organization slug * @param {string} tokenId - Token ID * @returns {Promise<void>} */ async revokeProjectToken(projectSlug, organizationSlug, tokenId) { if (!this.apiService) { throw new VizzlyError('API service not available', 'NO_API_SERVICE'); } try { await this.apiService.request(`/api/cli/organizations/${organizationSlug}/projects/${projectSlug}/tokens/${tokenId}`, { method: 'DELETE' }); } catch (error) { throw new VizzlyError(`Failed to revoke project token: ${error.message}`, 'TOKEN_REVOKE_FAILED', { originalError: error }); } } }