doclyft
Version:
CLI for DocLyft - Interactive documentation generator with hosted documentation support
734 lines (733 loc) • 32 kB
JavaScript
;
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();