UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

182 lines 8.18 kB
import fs from 'fs'; import path from 'path'; import { execSync, logger } from '../../utils/index.js'; import install from '../install/index.js'; import deploy from '../deploy/index.js'; const getStatusCodeMessage = response => { switch (response.status) { case 401: return 'Authentication failed. Please run: gh auth login'; case 403: if (response.headers.get('x-ratelimit-remaining') === '0') { const resetTime = response.headers.get('x-ratelimit-reset'); const resetDate = resetTime ? new Date(resetTime * 1000).toLocaleTimeString() : 'unknown'; return `Rate limit exceeded. Resets at: ${resetDate}`; } return 'Access forbidden. Check your GitHub permissions for the limebooth/atlas-cli repository.'; case 404: return 'Resource not found. Verify the repository path and branch exist.'; } }; const createGitHubApiError = async (response, context) => `GitHub API Error (${response.status}): ${context}\n` + `${getStatusCodeMessage(response) ?? ''}\n` + `Details: ${await response.text().catch(() => '')}`; const getGitHubToken = () => { try { const token = execSync('gh auth token', { stdio: 'pipe' }).toString().trim(); if (!token) { throw new Error('GitHub token is empty'); } return token; } catch (error) { throw new Error('Failed to get GitHub authentication token.\n' + 'Please authenticate with GitHub CLI by running: gh auth login\n' + `Error details: ${error.message}`); } }; const downloadFile = async (url, filePath = 'unknown') => { try { const response = await fetch(url); if (!response.ok) { const errorMessage = await createGitHubApiError(response, `Failed to download file: ${filePath}`); throw new Error(errorMessage); } return response.text(); } catch (error) { if (error.message.startsWith('GitHub API Error')) { throw error; } throw new Error(`Network error while downloading file: ${filePath}\n` + `URL: ${url}\n` + `Error: ${error.message}`); } }; const fetchGitHubDirectory = async (owner, repo, path, token) => { const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`; try { const response = await fetch(url, { headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github.object' } }); if (!response.ok) { const errorMessage = await createGitHubApiError(response, `Failed to fetch directory: ${path}\nRepository: ${owner}/${repo}`); throw new Error(errorMessage); } const data = await response.json(); if (data.type === 'dir' && data.entries) { return data.entries; } throw new Error(`Invalid response structure for directory: ${path}\n` + `Repository: ${owner}/${repo}\n` + `Expected a directory but got: ${data.type || 'unknown'}`); } catch (error) { if (error.message.startsWith('GitHub API Error') || error.message.startsWith('Invalid response structure')) { throw error; } throw new Error(`Network error while fetching directory: ${path}\n` + `Repository: ${owner}/${repo}\n` + `URL: ${url}\n` + `Error: ${error.message}`); } }; const downloadDirectory = async (owner, repo, branch, sourcePath, destPath, token) => { const contents = await fetchGitHubDirectory(owner, repo, sourcePath, token).catch(error => { logger.error(error); throw error; }); if (!Array.isArray(contents)) { throw new Error(`Unexpected response structure when fetching directory: ${sourcePath}\n` + `Expected an array of contents but received: ${typeof contents}`); } const downloadPromises = contents.map(async item => { const itemDestPath = path.join(destPath, item.name); if (item.type === 'file') { logger.log(`Downloading ${item.path}...`); const fileContent = await downloadFile(item.download_url, item.path); const dir = path.dirname(itemDestPath); try { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } catch (error) { throw new Error(`Failed to create directory: ${dir}\n` + `Error: ${error.message}\n` + 'Possible causes: Insufficient permissions or disk space'); } try { fs.writeFileSync(itemDestPath, fileContent, 'utf-8'); } catch (error) { throw new Error(`Failed to write file: ${itemDestPath}\n` + `Error: ${error.message}\n` + 'Possible causes: Insufficient permissions, disk space, or path too long'); } } else if (item.type === 'dir') { logger.log(`Creating directory ${item.path}...`); try { if (!fs.existsSync(itemDestPath)) { fs.mkdirSync(itemDestPath, { recursive: true }); } } catch (error) { throw new Error(`Failed to create directory: ${itemDestPath}\n` + `Error: ${error.message}\n` + 'Possible causes: Insufficient permissions or disk space'); } await downloadDirectory(owner, repo, branch, item.path, itemDestPath, token); } }); await Promise.all(downloadPromises); }; const updateFirebaseJson = () => { const firebaseJsonPath = './firebase.json'; if (!fs.existsSync(firebaseJsonPath)) { throw new Error('firebase.json not found in current directory.\n' + 'Please ensure you are in the root of your Firebase project.\n' + 'You can initialize a Firebase project by running: firebase init'); } const config = JSON.parse(fs.readFileSync(firebaseJsonPath).toString()); if (!config.functions || !Array.isArray(config.functions)) { throw new Error('Invalid firebase.json: The "functions" property must exist and be an array.\n' + 'Please ensure your firebase.json is properly configured for Cloud Functions.\n' + 'Run "firebase init functions" to set up Cloud Functions configuration.'); } if (config.functions.find(({ codebase }) => codebase === 'atlas-export')) { throw new Error('The atlas-export codebase is already configured in firebase.json.\n' + 'Aborting to prevent duplicate configuration.\n' + 'If you want to reinstall, please remove the existing atlas-export configuration first.'); } config.functions = [...config.functions, { predeploy: ['npm --prefix "$RESOURCE_DIR" run lint'], source: 'functions/atlas-export', codebase: 'atlas-export' }]; try { fs.writeFileSync(firebaseJsonPath, JSON.stringify(config, null, 4)); } catch (error) { throw new Error(`Failed to write updated firebase.json\nPath: ${firebaseJsonPath}\nError: ${error.message} `); } }; export default async (config = {}) => { const owner = 'limebooth'; const repo = 'atlas-cli'; const branch = 'main'; const sourcePath = 'assets/atlas-export'; const destPath = './functions/atlas-export'; logger.log('Initializing BigQuery export functions...'); try { const token = getGitHubToken(); try { if (!fs.existsSync(destPath)) { logger.log(`Creating ${destPath} directory...`); fs.mkdirSync(destPath, { recursive: true }); } } catch (error) { throw new Error(`Failed to create destination directory: ${destPath}\n` + `Error: ${error.message}\n` + 'Possible causes: Insufficient permissions or disk space'); } updateFirebaseJson(); if (config.copyLocal) { fs.cpSync(config.copyLocal, destPath, { recursive: true }); } else { await downloadDirectory(owner, repo, branch, sourcePath, destPath, token); } await install.functions({ codebase: 'atlas-export' }); await deploy.functions(['atlasExport.base']); logger.log('Successfully downloaded template files to ./functions'); logger.log('Continue the installation by reviewing and deploying the functions', 'banner'); } catch (error) { const errorContext = '\n================================================================================\n' + 'ATLAS-CLI INITIALIZATION ERROR\n' + '================================================================================\n' + `${error.message}\n` + '================================================================================\n'; logger.error(errorContext, true); } };