UNPKG

@vizzly-testing/cli

Version:

Visual review platform for UI developers and designers

244 lines (221 loc) 6.46 kB
/** * Global User Configuration Utilities * Manages ~/.vizzly/config.json for storing authentication tokens */ import { homedir } from 'os'; import { join, dirname, parse } from 'path'; import { readFile, writeFile, mkdir, chmod } from 'fs/promises'; import { existsSync } from 'fs'; import * as output from './output.js'; /** * Get the path to the global Vizzly directory * @returns {string} Path to ~/.vizzly */ export function getGlobalConfigDir() { return join(homedir(), '.vizzly'); } /** * Get the path to the global config file * @returns {string} Path to ~/.vizzly/config.json */ export function getGlobalConfigPath() { return join(getGlobalConfigDir(), 'config.json'); } /** * Ensure the global config directory exists with proper permissions * @returns {Promise<void>} */ async function ensureGlobalConfigDir() { let dir = getGlobalConfigDir(); if (!existsSync(dir)) { await mkdir(dir, { recursive: true, mode: 0o700 }); } } /** * Load the global configuration * @returns {Promise<Object>} Global config object */ export async function loadGlobalConfig() { try { let configPath = getGlobalConfigPath(); if (!existsSync(configPath)) { return {}; } let content = await readFile(configPath, 'utf-8'); return JSON.parse(content); } catch (error) { // If file doesn't exist or is corrupted, return empty config if (error.code === 'ENOENT') { return {}; } // Log warning about corrupted config but don't crash output.warn('Global config file is corrupted, ignoring'); return {}; } } /** * Save the global configuration * @param {Object} config - Configuration object to save * @returns {Promise<void>} */ export async function saveGlobalConfig(config) { await ensureGlobalConfigDir(); let configPath = getGlobalConfigPath(); let content = JSON.stringify(config, null, 2); // Write file with secure permissions (owner read/write only) await writeFile(configPath, content, { mode: 0o600 }); // Ensure permissions are set correctly (in case umask interfered) try { await chmod(configPath, 0o600); } catch (error) { // On Windows, chmod may not work as expected, but that's okay if (process.platform !== 'win32') { throw error; } } } /** * Clear all global configuration * @returns {Promise<void>} */ export async function clearGlobalConfig() { await saveGlobalConfig({}); } /** * Get authentication tokens from global config * @returns {Promise<Object|null>} Token object with accessToken, refreshToken, expiresAt, user, or null if not found */ export async function getAuthTokens() { let config = await loadGlobalConfig(); if (!config.auth || !config.auth.accessToken) { return null; } return config.auth; } /** * Save authentication tokens to global config * @param {Object} auth - Auth object with accessToken, refreshToken, expiresAt, user * @returns {Promise<void>} */ export async function saveAuthTokens(auth) { let config = await loadGlobalConfig(); config.auth = { accessToken: auth.accessToken, refreshToken: auth.refreshToken, expiresAt: auth.expiresAt, user: auth.user }; await saveGlobalConfig(config); } /** * Clear authentication tokens from global config * @returns {Promise<void>} */ export async function clearAuthTokens() { let config = await loadGlobalConfig(); delete config.auth; await saveGlobalConfig(config); } /** * Check if authentication tokens exist and are not expired * @returns {Promise<boolean>} True if valid tokens exist */ export async function hasValidTokens() { let auth = await getAuthTokens(); if (!auth || !auth.accessToken) { return false; } // Check if token is expired if (auth.expiresAt) { let expiresAt = new Date(auth.expiresAt); let now = new Date(); // Consider expired if within 5 minutes of expiry let bufferMs = 5 * 60 * 1000; if (now.getTime() >= expiresAt.getTime() - bufferMs) { return false; } } return true; } /** * Get the access token from global config if available * @returns {Promise<string|null>} Access token or null */ export async function getAccessToken() { let auth = await getAuthTokens(); return auth?.accessToken || null; } /** * Get project mapping for a directory * Walks up the directory tree to find the closest mapping * @param {string} directoryPath - Absolute path to project directory * @returns {Promise<Object|null>} Project data or null */ export async function getProjectMapping(directoryPath) { let config = await loadGlobalConfig(); if (!config.projects) { return null; } // Walk up the directory tree looking for a mapping let currentPath = directoryPath; let { root } = parse(currentPath); while (currentPath !== root) { if (config.projects[currentPath]) { return config.projects[currentPath]; } // Move to parent directory let parentPath = dirname(currentPath); if (parentPath === currentPath) { // We've reached the root break; } currentPath = parentPath; } return null; } /** * Save project mapping for a directory * @param {string} directoryPath - Absolute path to project directory * @param {Object} projectData - Project configuration * @param {string} projectData.token - Project API token (vzt_...) * @param {string} projectData.projectSlug - Project slug * @param {string} projectData.organizationSlug - Organization slug * @param {string} projectData.projectName - Project name */ export async function saveProjectMapping(directoryPath, projectData) { let config = await loadGlobalConfig(); if (!config.projects) { config.projects = {}; } config.projects[directoryPath] = { ...projectData, createdAt: new Date().toISOString() }; await saveGlobalConfig(config); } /** * Get all project mappings * @returns {Promise<Object>} Map of directory paths to project data */ export async function getProjectMappings() { let config = await loadGlobalConfig(); return config.projects || {}; } /** * Delete project mapping for a directory * @param {string} directoryPath - Absolute path to project directory */ export async function deleteProjectMapping(directoryPath) { let config = await loadGlobalConfig(); if (config.projects && config.projects[directoryPath]) { delete config.projects[directoryPath]; await saveGlobalConfig(config); } }