@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
182 lines • 8.18 kB
JavaScript
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);
}
};