UNPKG

qraft

Version:

A powerful CLI tool to qraft structured project setups from GitHub template repositories

466 lines 19.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RegistryManager = void 0; const rest_1 = require("@octokit/rest"); /** * RegistryManager handles GitHub API integration for remote template repositories */ class RegistryManager { constructor(config) { this.octokitInstances = new Map(); this.config = config; } /** * Parse a box reference string into registry and box name components * @param reference Box reference (e.g., "n8n", "myorg/n8n", "aws/lambda") * @param overrideRegistry Optional registry to override the parsed registry * @returns BoxReference Parsed reference information */ parseBoxReference(reference, overrideRegistry) { const parts = reference.split('/'); let registry; let boxName; if (overrideRegistry) { // If override registry is provided, use it and treat entire reference as box name registry = overrideRegistry; boxName = reference; } else if (parts.length === 1) { // Simple box name, use default registry registry = this.config.defaultRegistry; boxName = parts[0]; } else if (parts.length === 2) { // Could be either "registry/box" or "nested/box" format // First, check if the first part is a configured registry if (this.config.registries[parts[0]]) { // It's a registry/box format registry = parts[0]; boxName = parts[1]; } else { // Treat as nested box path in default registry registry = this.config.defaultRegistry; boxName = reference; // Keep the full path as box name } } else { // Multiple parts - could be "registry/nested/box" or just "nested/path/box" // Check if the first part is a configured registry if (this.config.registries[parts[0]]) { // It's a registry with nested path registry = parts[0]; boxName = parts.slice(1).join('/'); } else { // Treat entire path as nested box in default registry registry = this.config.defaultRegistry; boxName = reference; } } // Validate that the registry exists in configuration if (!this.config.registries[registry]) { throw new Error(`Registry '${registry}' is not configured. Available registries: ${Object.keys(this.config.registries).join(', ')}`); } return { registry, boxName, fullReference: reference }; } /** * Resolve registry name to full registry configuration * @param registryName Registry name or alias * @returns RegistryConfig Registry configuration */ resolveRegistry(registryName) { const registry = this.config.registries[registryName]; if (!registry) { throw new Error(`Registry '${registryName}' not found. Available registries: ${Object.keys(this.config.registries).join(', ')}`); } return registry; } /** * Get the effective registry for a box reference * @param reference Box reference * @param overrideRegistry Optional registry override * @returns string Effective registry name */ getEffectiveRegistry(reference, overrideRegistry) { if (overrideRegistry) { // Validate override registry exists if (!this.config.registries[overrideRegistry]) { throw new Error(`Override registry '${overrideRegistry}' is not configured`); } return overrideRegistry; } const parts = reference.split('/'); if (parts.length === 1) { // Simple box name, use default registry return this.config.defaultRegistry; } else { // Extract registry from reference const registryFromRef = parts.length === 2 ? parts[0] : parts.slice(0, -1).join('/'); // Validate registry exists if (!this.config.registries[registryFromRef]) { throw new Error(`Registry '${registryFromRef}' from reference '${reference}' is not configured`); } return registryFromRef; } } /** * Get or create an Octokit instance for a specific registry * @param registryName Name of the registry * @returns Promise<Octokit> Configured Octokit instance */ async getOctokitInstance(registryName) { if (this.octokitInstances.has(registryName)) { return this.octokitInstances.get(registryName); } const registryConfig = this.config.registries[registryName]; if (!registryConfig) { throw new Error(`Registry '${registryName}' not found in configuration`); } const octokitConfig = { baseUrl: registryConfig.baseUrl || 'https://api.github.com' }; // Add authentication if token is available const token = this.getAuthToken(registryName); if (token) { octokitConfig.auth = token; } const octokit = new rest_1.Octokit(octokitConfig); this.octokitInstances.set(registryName, octokit); return octokit; } /** * Get authentication token for a registry * @param registryName Name of the registry * @returns string | undefined Authentication token or undefined if not available */ getAuthToken(registryName) { const registryConfig = this.config.registries[registryName]; if (!registryConfig) { return undefined; } // Priority: registry-specific token > global token return registryConfig.token || this.config.globalToken; } /** * Check if a registry has authentication configured * @param registryName Name of the registry * @returns boolean True if authentication is available */ hasAuthentication(registryName) { return this.getAuthToken(registryName) !== undefined; } /** * Test authentication for a registry * @param registryName Name of the registry * @returns Promise<{authenticated: boolean, user?: string, error?: string}> Authentication test result */ async testAuthentication(registryName) { try { const octokit = await this.getOctokitInstance(registryName); // Try to get authenticated user info const { data: user } = await octokit.rest.users.getAuthenticated(); return { authenticated: true, user: user.login }; } catch (error) { if (error instanceof Error) { // Check if it's an authentication error if (error.message.includes('401') || error.message.includes('Bad credentials')) { return { authenticated: false, error: 'Invalid or expired authentication token' }; } // Check if no authentication was provided but required if (error.message.includes('403') && error.message.includes('rate limit')) { return { authenticated: false, error: 'No authentication provided - required for private repositories or to avoid rate limits' }; } return { authenticated: false, error: error.message }; } return { authenticated: false, error: 'Unknown authentication error' }; } } /** * Recursively discover all boxes in a repository * @param octokit GitHub API instance * @param owner Repository owner * @param repo Repository name * @param path Current path to search (default: '') * @param maxDepth Maximum recursion depth (default: 5) * @returns Promise<string[]> Array of box paths */ async discoverBoxesRecursively(octokit, owner, repo, path = '', maxDepth = 5) { if (maxDepth <= 0) { return []; } const boxes = []; try { const { data } = await octokit.rest.repos.getContent({ owner, repo, path }); if (!Array.isArray(data)) { return []; } for (const item of data) { if (item.type === 'dir') { const itemPath = path ? `${path}/${item.name}` : item.name; try { // Check if this directory contains manifest.json await octokit.rest.repos.getContent({ owner, repo, path: `${itemPath}/manifest.json` }); // If we get here, manifest.json exists - this is a box boxes.push(itemPath); } catch (error) { // No manifest.json in this directory, continue searching recursively const nestedBoxes = await this.discoverBoxesRecursively(octokit, owner, repo, itemPath, maxDepth - 1); boxes.push(...nestedBoxes); } } } } catch (error) { // If we can't read this directory, skip it return []; } return boxes; } /** * List all boxes available in a registry * @param registryName Name of the registry (optional, uses default if not provided) * @returns Promise<string[]> Array of box names */ async listBoxes(registryName) { const registry = registryName || this.config.defaultRegistry; const registryConfig = this.config.registries[registry]; if (!registryConfig) { throw new Error(`Registry '${registry}' not found`); } try { const octokit = await this.getOctokitInstance(registry); const [owner, repo] = registryConfig.repository.split('/'); // Use recursive discovery to find all boxes const boxes = await this.discoverBoxesRecursively(octokit, owner, repo); return boxes.sort(); } catch (error) { if (error instanceof Error) { // Check for authentication errors if (error.message.includes('401') || error.message.includes('Bad credentials')) { throw new Error(`Authentication failed for registry '${registry}'. Please check your GitHub token.`); } if (error.message.includes('403')) { if (error.message.includes('rate limit')) { throw new Error(`Rate limit exceeded for registry '${registry}'. Consider adding authentication to increase rate limits.`); } throw new Error(`Access denied to registry '${registry}'. This may be a private repository requiring authentication.`); } if (error.message.includes('404')) { throw new Error(`Repository not found for registry '${registry}'. Please check the repository name.`); } throw new Error(`Failed to list boxes from registry '${registry}': ${error.message}`); } throw new Error(`Failed to list boxes from registry '${registry}': Unknown error`); } } /** * Get box information from a registry * @param boxRef Box reference * @returns Promise<BoxInfo | null> Box information or null if not found */ async getBoxInfo(boxRef) { const registryConfig = this.config.registries[boxRef.registry]; if (!registryConfig) { throw new Error(`Registry '${boxRef.registry}' not found`); } try { const octokit = await this.getOctokitInstance(boxRef.registry); const [owner, repo] = registryConfig.repository.split('/'); // Get manifest.json const manifestResponse = await octokit.rest.repos.getContent({ owner, repo, path: `${boxRef.boxName}/manifest.json` }); if (Array.isArray(manifestResponse.data) || manifestResponse.data.type !== 'file') { return null; } // Decode manifest content const manifestContent = Buffer.from(manifestResponse.data.content, 'base64').toString('utf-8'); const manifest = JSON.parse(manifestContent); // Validate required fields if (!manifest.name || !manifest.description || !manifest.author || !manifest.version) { throw new Error(`Invalid manifest for box '${boxRef.boxName}': missing required fields`); } // Get list of files in the box const files = await this.getBoxFiles(boxRef); return { manifest, path: `${registryConfig.repository}/${boxRef.boxName}`, files }; } catch (error) { if (error instanceof Error) { if (error.message.includes('404')) { return null; // Box not found } // Check for authentication errors if (error.message.includes('401') || error.message.includes('Bad credentials')) { throw new Error(`Authentication failed for registry '${boxRef.registry}'. Please check your GitHub token.`); } if (error.message.includes('403')) { if (error.message.includes('rate limit')) { throw new Error(`Rate limit exceeded for registry '${boxRef.registry}'. Consider adding authentication to increase rate limits.`); } throw new Error(`Access denied to box '${boxRef.fullReference}'. This may be a private repository requiring authentication.`); } } throw error; } } /** * Get list of files in a box * @param boxRef Box reference * @returns Promise<string[]> Array of relative file paths */ async getBoxFiles(boxRef) { const registryConfig = this.config.registries[boxRef.registry]; if (!registryConfig) { throw new Error(`Registry '${boxRef.registry}' not found`); } try { const octokit = await this.getOctokitInstance(boxRef.registry); const [owner, repo] = registryConfig.repository.split('/'); const files = []; const scanDirectory = async (dirPath, relativePath = '') => { try { const { data } = await octokit.rest.repos.getContent({ owner, repo, path: dirPath }); if (!Array.isArray(data)) { // If data is not an array, it might be a single file if (data.type === 'file') { const itemRelativePath = relativePath ? `${relativePath}/${data.name}` : data.name; // Skip manifest.json in root if (!(data.name === 'manifest.json' && relativePath === '')) { files.push(itemRelativePath); } } return; } for (const item of data) { const itemRelativePath = relativePath ? `${relativePath}/${item.name}` : item.name; // Skip manifest.json in root if (item.name === 'manifest.json' && relativePath === '') { continue; } if (item.type === 'dir') { await scanDirectory(`${dirPath}/${item.name}`, itemRelativePath); } else { files.push(itemRelativePath); } } } catch (scanError) { // Log the error but don't fail the entire operation console.warn(`Warning: Failed to scan directory '${dirPath}': ${scanError instanceof Error ? scanError.message : 'Unknown error'}`); // If it's a 404, the directory might not exist, which is okay if (scanError instanceof Error && scanError.message.includes('404')) { return; } // For other errors, we should still throw to avoid silent failures throw scanError; } }; await scanDirectory(boxRef.boxName); return files.sort(); } catch (error) { throw new Error(`Failed to get files for box '${boxRef.fullReference}': ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Download a file from a box * @param boxRef Box reference * @param filePath Relative file path within the box * @returns Promise<Buffer> File content as buffer */ async downloadFile(boxRef, filePath) { const registryConfig = this.config.registries[boxRef.registry]; if (!registryConfig) { throw new Error(`Registry '${boxRef.registry}' not found`); } try { const octokit = await this.getOctokitInstance(boxRef.registry); const [owner, repo] = registryConfig.repository.split('/'); const { data } = await octokit.rest.repos.getContent({ owner, repo, path: `${boxRef.boxName}/${filePath}` }); if (Array.isArray(data) || data.type !== 'file') { throw new Error(`File '${filePath}' is not a regular file`); } return Buffer.from(data.content, 'base64'); } catch (error) { throw new Error(`Failed to download file '${filePath}' from box '${boxRef.fullReference}': ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Check if a box exists in a registry * @param boxRef Box reference * @returns Promise<boolean> True if box exists */ async boxExists(boxRef) { try { const boxInfo = await this.getBoxInfo(boxRef); return boxInfo !== null; } catch (error) { return false; } } /** * Get the default registry name * @returns string Default registry name */ getDefaultRegistry() { return this.config.defaultRegistry; } /** * Get all configured registries * @returns Record<string, RegistryConfig> Map of registry configurations */ getRegistries() { return { ...this.config.registries }; } } exports.RegistryManager = RegistryManager; //# sourceMappingURL=registryManager.js.map