UNPKG

doclyft

Version:

CLI for DocLyft - Interactive documentation generator with hosted documentation support

734 lines (733 loc) 32 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const config_1 = __importDefault(require("./config")); const axios_1 = __importDefault(require("axios")); const chalk_1 = __importDefault(require("chalk")); const config_2 = require("../config"); const errors_1 = require("../utils/errors"); const sanitization_1 = require("../utils/sanitization"); const auth_1 = require("../middleware/auth"); class ApiClient { getHeaders() { const token = config_1.default.get('token'); if (!token) { throw new auth_1.AuthError(auth_1.AUTH_ERRORS.NOT_LOGGED_IN, 'NO_TOKEN'); } return { 'Authorization': `Bearer ${config_2.SUPABASE_ANON_KEY}`, 'X-API-Key': token, 'apikey': config_2.SUPABASE_ANON_KEY, }; } getAuthHeaders() { const token = config_1.default.get('token'); if (!token) { throw new auth_1.AuthError(auth_1.AUTH_ERRORS.NOT_LOGGED_IN, 'NO_TOKEN'); } return { 'Authorization': `Bearer ${token}`, 'apikey': config_2.SUPABASE_ANON_KEY, }; } async analyzeRepository(params) { try { const headers = this.getHeaders(); // Use CLI-specific endpoint for team analysis if (params.team_analysis) { const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/cli-analyze-repo`, { repo_owner: params.repo_owner, repo_name: params.repo_name, repo_full_name: params.repo_full_name, default_branch: params.default_branch }, { headers }); return response.data; } // Use regular endpoint for personal repositories const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/analyze-repo`, params, { headers }); return response.data; } catch (error) { this.handleError(error, 'repository analysis'); } } async saveAnalysis(analysisData, userId, userEmail) { try { const headers = this.getHeaders(); const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/save-analysis`, { ...analysisData, user_id: userId, user_email: userEmail }, { headers }); return response.data; } catch (error) { this.handleError(error, 'saving analysis'); } } async generateReadme(repositoryId) { try { const headers = this.getHeaders(); const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/generate-readme`, { repository_id: repositoryId, }, { headers }); // Handle streaming response if (typeof response.data === 'string') { return response.data; } // If it's a stream, we need to handle it differently return response.data; } catch (error) { this.handleError(error, 'README generation'); } } async generateDocs(analysisData, userId, userEmail, sections) { try { const headers = this.getHeaders(); const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/generate-docs`, { ...analysisData, user_id: userId, user_email: userEmail, sections: sections || [ 'overview', 'installation', 'usage', 'apiReference', 'testing', 'projectStructure', 'deployment', 'troubleshooting', 'contributing', 'changelog', 'license' ] }, { headers }); return response.data; } catch (error) { this.handleError(error, 'documentation generation'); } } async pushDocs(analysisData, options) { try { const headers = this.getHeaders(); // Extract repository and documentation from analysis data const repository = analysisData.repository; const documentation = analysisData.documentation || {}; const userId = config_1.default.get('user_id'); if (!repository) { throw new errors_1.CLIError('Repository information not found in analysis data'); } if (!userId) { throw new errors_1.CLIError('User ID not found. Please run `doclyft login` first.'); } // Check if this is a team repository const repoFullName = repository.full_name; let githubToken; let isTeamRepo = false; try { const teamRepos = await this.getTeamRepositories(); isTeamRepo = teamRepos.some(repo => repo.full_name === repoFullName); if (isTeamRepo) { console.log(`🏢 Repository ${repoFullName} is a team repository - using team owner's GitHub token for push`); // For team repositories, the push-docs function will handle getting the team owner's token githubToken = undefined; } else { // For personal repositories, get the user's GitHub token githubToken = await this.getGitHubToken(); } } catch (error) { // If team check fails, fallback to personal token githubToken = await this.getGitHubToken(); } // Use team-aware push endpoint for team repositories const endpoint = isTeamRepo ? '/functions/v1/cli-push-docs' : '/functions/v1/push-docs'; const requestData = isTeamRepo ? { repository, documentation, pushOptions: { exportType: options?.export_type || 'readme', branchName: options?.branch, commitMessage: options?.commit_message || 'Update documentation via DocLyft CLI', prTitle: options?.pr_title, prDescription: options?.pr_description } } : { repository, documentation, pushOptions: { exportType: options?.export_type || 'readme', branchName: options?.branch, commitMessage: options?.commit_message || 'Update documentation via DocLyft CLI', prTitle: options?.pr_title, prDescription: options?.pr_description }, userId, githubToken }; await axios_1.default.post(`${config_2.SUPABASE_URL}${endpoint}`, requestData, { headers }); } catch (error) { this.handleError(error, 'pushing documentation'); } } async syncActivities(activities, cliSessionId) { try { const headers = this.getHeaders(); const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/sync-cli-activity`, { activities, cli_session_id: cliSessionId || `cli_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` }, { headers }); return { ok: response.status >= 200 && response.status < 300, json: async () => response.data }; } catch (error) { console.error('Activity sync failed:', error); return { ok: false, json: async () => ({ error: 'Activity sync failed' }) }; } } async logPushActivity(pushData) { try { const headers = this.getHeaders(); const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/log-push-activity`, { ...pushData, push_source: 'cli' }, { headers }); return { ok: response.status >= 200 && response.status < 300, json: async () => response.data }; } catch (error) { console.error('Push activity logging failed:', error); return { ok: false, json: async () => ({ error: 'Push activity logging failed' }) }; } } async post(endpoint, data) { try { const headers = this.getHeaders(); const url = endpoint.startsWith('http') ? endpoint : `${config_2.SUPABASE_URL}/functions/v1${endpoint}`; const response = await axios_1.default.post(url, data, { headers }); return { ok: response.status >= 200 && response.status < 300, json: async () => response.data }; } catch (error) { console.error(`API POST to ${endpoint} failed:`, error); return { ok: false, json: async () => ({ error: `API POST failed` }) }; } } // Check if GitHub token is available (backend or local) without throwing errors async hasGitHubToken() { try { // First, try backend token const backendToken = await this.getStoredGitHubToken(); if (backendToken) { try { const { validateGitHubToken } = await Promise.resolve().then(() => __importStar(require('../utils/validation'))); validateGitHubToken(backendToken); return { hasToken: true, source: 'backend', isValid: true }; } catch (validationError) { return { hasToken: true, source: 'backend', isValid: false }; } } // Fallback to local token const localToken = config_1.default.get('github_token'); if (localToken) { try { const { validateGitHubToken } = await Promise.resolve().then(() => __importStar(require('../utils/validation'))); validateGitHubToken(localToken); return { hasToken: true, source: 'local', isValid: true }; } catch (validationError) { return { hasToken: true, source: 'local', isValid: false }; } } return { hasToken: false, source: 'none' }; } catch (error) { // If there's any error, fall back to checking local token only const localToken = config_1.default.get('github_token'); if (localToken) { try { const { validateGitHubToken } = await Promise.resolve().then(() => __importStar(require('../utils/validation'))); validateGitHubToken(localToken); return { hasToken: true, source: 'local', isValid: true }; } catch (validationError) { return { hasToken: true, source: 'local', isValid: false }; } } return { hasToken: false, source: 'none' }; } } async getGitHubToken() { // First, try to get token from backend (user already authenticated via web platform) const backendToken = await this.getStoredGitHubToken(); if (backendToken) { try { const { validateGitHubToken } = await Promise.resolve().then(() => __importStar(require('../utils/validation'))); validateGitHubToken(backendToken); return backendToken; } catch (validationError) { // Backend token is invalid, continue to local token check console.warn('Backend GitHub token is invalid, checking local configuration...'); } } // Fallback to locally stored token const storedToken = config_1.default.get('github_token'); if (storedToken) { // Validate the token format before using it try { const { validateGitHubToken } = await Promise.resolve().then(() => __importStar(require('../utils/validation'))); validateGitHubToken(storedToken); return storedToken; } catch (validationError) { throw new errors_1.GitHubTokenError(`Stored GitHub token is invalid: ${validationError instanceof Error ? validationError.message : 'Unknown error'}. Please update it using: doclyft config set github_token <your_token>`); } } throw new errors_1.GitHubTokenError('GitHub token not found. Please authenticate with GitHub on the DocLyft platform first, then run `doclyft login` again. Alternatively, you can set a token manually using: doclyft github-token'); } // Get GitHub token from backend (user already authenticated via web platform) async getStoredGitHubToken() { try { const token = config_1.default.get('token'); if (!token) { return null; } const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/user-github-token`, { apiKey: token }, { headers: { 'Authorization': `Bearer ${config_2.SUPABASE_ANON_KEY}`, 'Content-Type': 'application/json', 'apikey': config_2.SUPABASE_ANON_KEY, } }); return response.data.github_token || null; } catch (error) { // If no token found or error, return null (fallback to manual setup) return null; } } async verifyToken(token) { try { const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/cli-auth`, {}, { headers: { 'Authorization': `Bearer ${config_2.SUPABASE_ANON_KEY}`, 'X-API-Key': token, 'apikey': config_2.SUPABASE_ANON_KEY, }, timeout: 10000, // 10 second timeout }); return response.status === 200; } catch (error) { // Always log errors for debugging this issue if (axios_1.default.isAxiosError(error)) { console.error('❌ Token verification failed:', { status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data, message: error.message, timeout: error.code === 'ECONNABORTED', url: error.config?.url }); // Provide specific guidance based on error type if (error.response?.status === 500) { console.log(chalk_1.default.yellow('⚠️ Server error - this indicates a backend issue')); console.log(chalk_1.default.blue(' The API key format is correct, but the server cannot validate it')); console.log(chalk_1.default.blue(' This could be a temporary service issue or the API key may not exist')); } } else { console.error('❌ Non-axios error:', error); } return false; } } // Test connectivity to the API without authentication async testConnectivity() { const startTime = Date.now(); try { // Try the health check endpoint via API gateway const response = await axios_1.default.get(`${config_2.SUPABASE_URL}/functions/v1/api-gateway/api/v1/health`, { timeout: 5000 }); const latency = Date.now() - startTime; return { success: true, latency }; } catch (error) { const latency = Date.now() - startTime; if (axios_1.default.isAxiosError(error)) { return { success: false, error: error.code === 'ECONNABORTED' ? 'Connection timeout' : error.message, latency }; } return { success: false, error: 'Unknown error', latency }; } } // Helper method to get user info from token async getUserInfo(tokenOverride) { try { const token = tokenOverride || config_1.default.get('token'); if (!token) { throw new Error('Not authenticated. Please run `doclyft login` first.'); } const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/cli-auth`, {}, { headers: { 'Authorization': `Bearer ${config_2.SUPABASE_ANON_KEY}`, 'X-API-Key': token, 'apikey': config_2.SUPABASE_ANON_KEY, }, timeout: 10000, // 10 second timeout }); return { user_id: response.data.user_id, user_email: response.data.user_email || config_1.default.get('user_email') || 'Unknown' }; } catch (error) { this.handleError(error, 'getting user information'); } } async listAnalyzedRepos() { try { const headers = this.getHeaders(); const response = await axios_1.default.get(`${config_2.SUPABASE_URL}/functions/v1/list-analyzed-repos`, { headers }); return response.data; } catch (error) { this.handleError(error, 'listing analyzed repositories'); } } async getAnalysisData(repositoryId) { try { const headers = this.getHeaders(); const response = await axios_1.default.get(`${config_2.SUPABASE_URL}/functions/v1/get-analysis-data/${repositoryId}`, { headers }); return response.data; } catch (error) { this.handleError(error, 'getting analysis data'); } } async getGitHubRepos() { try { // First, try to get team repositories if user is a team member const teamRepos = await this.getTeamRepositories(); if (teamRepos.length > 0) { return teamRepos; } // Fallback to personal repositories if no team repos or not a team member const githubToken = await this.getGitHubToken(); const response = await axios_1.default.get('https://api.github.com/user/repos', { headers: { 'Authorization': `Bearer ${githubToken}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'DocLyft-CLI/1.0' }, params: { sort: 'updated', per_page: 100, affiliation: 'owner,collaborator' }, timeout: 10000 }); return response.data.map((repo) => ({ name: repo.name, full_name: repo.full_name, private: repo.private, description: repo.description })); } catch (error) { this.handleError(error, 'fetching GitHub repositories'); } } // Get repositories accessible to team members using API key authentication async getTeamRepositories() { try { const headers = this.getHeaders(); const response = await axios_1.default.get(`${config_2.SUPABASE_URL}/functions/v1/cli-team-repositories`, { headers }); if (response.data.success && response.data.repositories) { return response.data.repositories.map((repo) => ({ name: repo.name, full_name: repo.full_name, private: repo.private, description: repo.description })); } return []; } catch (error) { // If team repository endpoint fails, silently return empty array // This allows fallback to personal repositories return []; } } // Get GitHub token for a specific repository (team-aware) async getGitHubTokenForRepo(repoFullName) { try { // First, check if this repo is a team repository const teamRepos = await this.getTeamRepositories(); const isTeamRepo = teamRepos.some(repo => repo.full_name === repoFullName); if (isTeamRepo) { // For team repositories, we'll get the team owner's token through the backend console.log(`🔍 Repository ${repoFullName} is a team repository - using team owner's GitHub token`); return 'TEAM_REPO_TOKEN_PLACEHOLDER'; } // For personal repositories, use the existing logic return await this.getGitHubToken(); } catch (error) { // If team check fails, fallback to personal token return await this.getGitHubToken(); } } // Get repository information including default branch (team-aware) async getRepositoryInfo(repoFullName) { try { // Check if this is a team repository const teamRepos = await this.getTeamRepositories(); const teamRepo = teamRepos.find(repo => repo.full_name === repoFullName); if (teamRepo) { // For team repositories, we can get the default branch from GitHub API using team owner's token // But since we can't directly access the team owner's token from CLI, we'll use a CLI endpoint const headers = this.getHeaders(); const response = await axios_1.default.get(`${config_2.SUPABASE_URL}/functions/v1/cli-repo-info`, { headers, params: { repo: repoFullName } }); if (response.data.success) { return { default_branch: response.data.default_branch, private: response.data.private }; } // Fallback - try to guess from common patterns return { default_branch: 'main', private: true }; } // For personal repositories, use the existing logic with personal token const githubToken = await this.getGitHubToken(); const response = await axios_1.default.get(`https://api.github.com/repos/${repoFullName}`, { headers: { 'Authorization': `Bearer ${githubToken}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'DocLyft-CLI/1.0' }, timeout: 10000 }); return { default_branch: response.data.default_branch || 'main', private: response.data.private || false }; } catch (error) { console.warn(`Could not get repository info for ${repoFullName}, using defaults`); return { default_branch: 'main', private: false }; } } // Hosting API methods async enableHosting(options) { try { const headers = this.getHeaders(); const userId = config_1.default.get('user_id'); if (!userId) { throw new errors_1.CLIError('User ID not found. Please run `doclyft login` first.'); } const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/cli-hosting`, { action: 'enable', user_id: userId, repository: options.repository, subdomain: options.subdomain, auto_sync: options.auto_sync, custom_domain: options.custom_domain, force: options.force }, { headers }); return response.data; } catch (error) { this.handleError(error, 'enabling hosting'); } } async disableHosting(repository) { try { const headers = this.getHeaders(); const userId = config_1.default.get('user_id'); if (!userId) { throw new errors_1.CLIError('User ID not found. Please run `doclyft login` first.'); } const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/cli-hosting`, { action: 'disable', user_id: userId, repository }, { headers }); return response.data; } catch (error) { this.handleError(error, 'disabling hosting'); } } async getHostingStatus(repository) { try { const headers = this.getHeaders(); const userId = config_1.default.get('user_id'); if (!userId) { throw new errors_1.CLIError('User ID not found. Please run `doclyft login` first.'); } const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/cli-hosting`, { action: repository ? 'status' : 'list', user_id: userId, repository }, { headers }); return response.data; } catch (error) { this.handleError(error, 'getting hosting status'); } } async deployHosting(repository) { try { const headers = this.getHeaders(); const userId = config_1.default.get('user_id'); if (!userId) { throw new errors_1.CLIError('User ID not found. Please run `doclyft login` first.'); } const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/cli-hosting`, { action: 'deploy', user_id: userId, repository }, { headers }); return response.data; } catch (error) { this.handleError(error, 'deploying hosting'); } } async configureHosting(repository, options) { try { const headers = this.getHeaders(); const userId = config_1.default.get('user_id'); if (!userId) { throw new errors_1.CLIError('User ID not found. Please run `doclyft login` first.'); } const requestData = { action: 'config', user_id: userId, repository }; if (options.auto_sync !== undefined) { requestData.auto_sync = options.auto_sync; } if (options.custom_domain !== undefined) { requestData.custom_domain = options.custom_domain; } const response = await axios_1.default.post(`${config_2.SUPABASE_URL}/functions/v1/cli-hosting`, requestData, { headers }); return response.data; } catch (error) { this.handleError(error, 'configuring hosting'); } } handleError(error, context) { if (axios_1.default.isAxiosError(error)) { const axiosError = error; if (!axiosError.response) { throw new errors_1.NetworkError(`Network error during ${context}: ${axiosError.message}`); } const status = axiosError.response.status; const responseData = axiosError.response.data; const errorMessage = responseData?.error || responseData?.message || axiosError.message; // Log detailed error info for debugging 400 errors with sanitized data if (status === 400) { console.error('🔍 400 Error Details:', { url: axiosError.config?.url, method: axiosError.config?.method, data: (0, sanitization_1.sanitizeRequestData)(axiosError.config?.data), responseData: (0, sanitization_1.sanitizeResponseData)(responseData), headers: (0, sanitization_1.sanitizeHeaders)(axiosError.config?.headers) }); } switch (status) { case 400: throw new errors_1.CLIError(`Invalid request data during ${context}: ${errorMessage}`); case 401: throw new errors_1.AuthenticationError(`Authentication failed during ${context}: ${errorMessage}`); case 403: throw new errors_1.AuthenticationError(`Access denied during ${context}: ${errorMessage}`); case 404: throw new errors_1.CLIError(`Resource not found during ${context}: ${errorMessage}`); case 429: throw new errors_1.CLIError(`Rate limit exceeded during ${context}. Please wait and try again.`); case 422: if (context.includes('GitHub')) { throw new errors_1.GitHubTokenError(`GitHub API error: ${errorMessage}`); } throw new errors_1.CLIError(`Validation error during ${context}: ${errorMessage}`); case 500: case 502: case 503: case 504: throw new errors_1.CLIError(`Server error during ${context}: ${errorMessage}. Please try again later.`); default: throw new errors_1.CLIError(`HTTP ${status} error during ${context}: ${errorMessage}`); } } if (error instanceof Error) { throw new errors_1.CLIError(`Error during ${context}: ${error.message}`); } throw new errors_1.CLIError(`Unknown error during ${context}`); } } exports.default = new ApiClient();