UNPKG

doclyft

Version:

CLI for DocLyft - Interactive documentation generator with hosted documentation support

1,043 lines โ€ข 53.5 kB
"use strict"; /** * Slash command handler for interactive CLI session * Maps slash commands to existing CLI functionality */ 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 }); exports.SlashCommandHandler = void 0; const chalk_1 = __importDefault(require("chalk")); const prompts_1 = __importDefault(require("prompts")); const config_1 = __importDefault(require("./config")); const api_1 = __importDefault(require("./api")); const auth_1 = __importDefault(require("../middleware/auth")); const activity_logger_1 = require("../utils/activity-logger"); const ora_1 = __importDefault(require("ora")); const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const crypto_1 = __importDefault(require("crypto")); class SlashCommandHandler { constructor() { this.activityLogger = new activity_logger_1.ActivityLogger(); } /** * Execute a slash command */ async execute(command, args) { switch (command) { case 'help': await this.showHelp(); break; case 'status': await this.showStatus(); break; case 'login': await this.handleLogin(); break; case 'logout': await this.handleLogout(); break; case 'analyze': await this.handleAnalyze(args); break; case 'generate': await this.handleGenerate(args); break; case 'push': await this.handlePush(args); break; case 'repos': await this.handleRepos(args); break; case 'history': await this.handleHistory(args); break; case 'config': await this.handleConfig(args); break; case 'test': await this.handleTest(); break; case 'github-token': await this.handleGitHubToken(); break; case 'analyses': await this.handleAnalyses(); break; default: console.log(chalk_1.default.red(`โŒ Unknown command: /${command}`)); console.log(chalk_1.default.blue('๐Ÿ’ก Type /help to see available commands')); } } /** * Show help information */ async showHelp() { console.log(chalk_1.default.blue('\n๐Ÿ“– DocLyft Interactive CLI Commands\n')); console.log(chalk_1.default.cyan('Authentication:')); console.log(' /login Authenticate with DocLyft API token'); console.log(' /logout Remove stored authentication'); console.log(' /status Show current authentication and configuration status'); console.log(chalk_1.default.cyan('\nRepository Analysis:')); console.log(' /analyze repo Analyze a GitHub repository'); console.log(' /analyze list List previously analyzed repositories'); console.log(' /analyze select Select and load a previously analyzed repository'); console.log(' /analyses View and select previous analyses'); console.log(chalk_1.default.cyan('\nDocumentation Generation:')); console.log(' /generate readme Generate a README file'); console.log(' /generate docs Generate full documentation'); console.log(' /generate list List all generated READMEs'); console.log(chalk_1.default.cyan('\nGitHub Integration:')); console.log(' /push Push documentation to GitHub (interactive)'); console.log(' /repos List your GitHub repositories'); console.log(' /github-token Set and validate GitHub personal access token'); console.log(' /test Test GitHub token and connectivity'); console.log(chalk_1.default.cyan('\nConfiguration & History:')); console.log(' /config list List all configuration values'); console.log(' /config set <k> <v> Set a configuration value'); console.log(' /config get <key> Get a configuration value'); console.log(' /config clear Clear all configuration'); console.log(' /history View history of CLI actions'); console.log(chalk_1.default.cyan('\nSession Control:')); console.log(' /help Show this help message'); console.log(' /exit Exit interactive session'); console.log(chalk_1.default.blue('\n๐Ÿ’ก Examples:')); console.log(chalk_1.default.gray(' /analyze repo facebook/react')); console.log(chalk_1.default.gray(' /generate readme')); console.log(chalk_1.default.gray(' /push')); console.log(chalk_1.default.gray(' /config set github_token ghp_xxxx')); } /** * Show current status */ async showStatus() { console.log(chalk_1.default.blue('\n๐Ÿ“Š DocLyft CLI Status')); // Check authentication const authInfo = auth_1.default.getAuthInfo(); if (authInfo.token && authInfo.user_email) { console.log(chalk_1.default.green('โœ… Authenticated with DocLyft')); console.log(` User: ${authInfo.user_email}`); } else { console.log(chalk_1.default.red('โŒ Not authenticated')); console.log(chalk_1.default.blue(' Use /login to authenticate')); } // Check GitHub token try { const tokenInfo = await api_1.default.hasGitHubToken(); if (tokenInfo.hasToken && tokenInfo.isValid) { const sourceText = tokenInfo.source === 'backend' ? '(from platform)' : '(local config)'; console.log(chalk_1.default.green(`โœ… GitHub token available ${sourceText}`)); } else if (tokenInfo.hasToken && !tokenInfo.isValid) { console.log(chalk_1.default.red('โŒ GitHub token invalid')); console.log(chalk_1.default.blue(' Use /github-token to set a valid token')); } else { console.log(chalk_1.default.yellow('โš ๏ธ No GitHub token configured')); console.log(chalk_1.default.blue(' Use /github-token to set up GitHub access')); } } catch (error) { console.log(chalk_1.default.yellow('โš ๏ธ Could not check GitHub token status')); } // Show last analysis const lastAnalysisId = config_1.default.get('last_analysis_id'); const lastRepoName = config_1.default.get('last_repo_name'); if (lastAnalysisId && lastRepoName) { console.log(chalk_1.default.green('โœ… Last analysis available')); console.log(` Repository: ${lastRepoName}`); console.log(` Analysis ID: ${lastAnalysisId}`); } else { console.log(chalk_1.default.yellow('โš ๏ธ No recent analysis')); console.log(chalk_1.default.blue(' Use /analyze repo to get started')); } console.log(chalk_1.default.blue('\n๐Ÿ’ก Quick workflow:')); console.log(chalk_1.default.cyan(' /analyze repo owner/name โ†’ /generate readme โ†’ /push')); } /** * Handle login command */ async handleLogin() { console.log(chalk_1.default.blue('๐Ÿ” DocLyft Authentication')); const { token } = await (0, prompts_1.default)({ type: 'password', name: 'token', message: 'Enter your DocLyft API token:', }); if (!token) { console.log(chalk_1.default.yellow('Login cancelled')); return; } const cleanToken = token.trim(); if (!cleanToken.startsWith('dk_prod_')) { console.log(chalk_1.default.red('โŒ Invalid token format.')); console.log(chalk_1.default.yellow('๐Ÿ’ก DocLyft API tokens must start with "dk_prod_"')); console.log(chalk_1.default.blue(' Get your API token from: https://doclyft.com/dashboard/api-keys')); return; } if (cleanToken.length < 20) { console.log(chalk_1.default.red('โŒ Token appears to be incomplete.')); return; } const spinner = (0, ora_1.default)('Verifying token...').start(); try { const isValid = await api_1.default.verifyToken(cleanToken); if (!isValid) { spinner.fail(chalk_1.default.red('Invalid API token or connection failed.')); return; } const userInfo = await api_1.default.getUserInfo(cleanToken); config_1.default.set('token', cleanToken); config_1.default.set('user_id', userInfo.user_id); config_1.default.set('user_email', userInfo.user_email); // Create session const SessionManager = (await Promise.resolve().then(() => __importStar(require('./session')))).default; await SessionManager.createSession(cleanToken, userInfo.user_id, userInfo.user_email); spinner.succeed(chalk_1.default.green('Successfully logged in!')); console.log(chalk_1.default.blue(` Logged in as: ${userInfo.user_email}`)); console.log(chalk_1.default.blue(' Use /status to see available commands')); } catch (error) { spinner.fail(chalk_1.default.red(`Login failed: ${error instanceof Error ? error.message : 'Unknown error'}`)); } } /** * Handle logout command */ async handleLogout() { config_1.default.delete('token'); config_1.default.delete('user_id'); config_1.default.delete('user_email'); config_1.default.delete('github_token'); try { const SessionManager = (await Promise.resolve().then(() => __importStar(require('./session')))).default; await SessionManager.clearSession(); } catch (error) { // Ignore session manager errors } console.log(chalk_1.default.green('โœ… Successfully logged out!')); } /** * Handle analyze command */ async handleAnalyze(args) { // Require authentication await auth_1.default.requireAuth(); const subcommand = args[0] || 'repo'; switch (subcommand) { case 'repo': await this.analyzeRepo(args.slice(1)); break; case 'list': await this.analyzeList(); break; case 'select': await this.analyzeSelect(); break; default: console.log(chalk_1.default.red(`โŒ Unknown analyze subcommand: ${subcommand}`)); console.log(chalk_1.default.blue('๐Ÿ’ก Available: repo, list, select')); } } async analyzeRepo(args) { const { user_id, user_email } = auth_1.default.getAuthInfo(); // Get repo name from args or prompt let repoName = args[0]; if (!repoName) { const response = await (0, prompts_1.default)({ type: 'text', name: 'repo', message: 'Enter repository (owner/name):', validate: (value) => { if (!value.includes('/')) { return 'Repository must be in format owner/name (e.g., facebook/react)'; } return true; } }); repoName = response.repo; } if (!repoName) return; // Check if this is a team repository first let isTeamRepo = false; try { const teamRepos = await api_1.default.getTeamRepositories(); isTeamRepo = teamRepos.some(repo => repo.full_name === repoName); if (isTeamRepo) { console.log(chalk_1.default.green(`๐Ÿข Repository ${repoName} is a team repository - using team owner's GitHub token`)); } } catch (error) { // Continue with personal repo flow if team check fails } // Get GitHub token (only needed for non-team repositories) let githubToken; if (!isTeamRepo) { const tokenStatus = await api_1.default.hasGitHubToken(); if (tokenStatus.hasToken) { try { githubToken = await api_1.default.getGitHubToken(); console.log(chalk_1.default.green(`โœ… Using GitHub token from ${tokenStatus.source}`)); } catch (error) { console.log(chalk_1.default.yellow(`โš ๏ธ Failed to get GitHub token: ${error instanceof Error ? error.message : 'Unknown error'}`)); } } if (!githubToken) { console.log(chalk_1.default.yellow('โš ๏ธ No GitHub token found. You need a GitHub Personal Access Token.')); console.log(chalk_1.default.blue('Create one at: https://github.com/settings/personal-access-tokens/new')); const response = await (0, prompts_1.default)({ type: 'password', name: 'token', message: 'Enter GitHub personal access token:', }); githubToken = response.token; if (!githubToken) return; } } const [owner, name] = repoName.split('/'); // Auto-detect default branch console.log(chalk_1.default.blue('๐Ÿ” Auto-detecting default branch...')); let defaultBranch = 'main'; try { const repoInfo = await api_1.default.getRepositoryInfo(repoName); defaultBranch = repoInfo.default_branch; console.log(chalk_1.default.green(`โœ… Detected default branch: ${defaultBranch}`)); } catch (error) { console.log(chalk_1.default.yellow('โš ๏ธ Could not detect default branch, using "main"')); } const spinner = (0, ora_1.default)('Analyzing repository...').start(); try { await this.activityLogger.init(); const sessionId = crypto_1.default.randomUUID(); const analysis = await api_1.default.analyzeRepository({ github_token: isTeamRepo ? undefined : githubToken, user_id: user_id, user_email: user_email, repo_owner: owner, repo_name: name, repo_full_name: repoName, default_branch: defaultBranch, team_analysis: isTeamRepo }); const savedAnalysis = await api_1.default.saveAnalysis(analysis, user_id, user_email); await this.activityLogger.logActivity({ type: 'analyze', timestamp: new Date().toISOString(), repo: repoName, analysis_id: savedAnalysis.analysis_id, user_id: user_id }); spinner.succeed(chalk_1.default.green('Repository analyzed successfully!')); console.log(chalk_1.default.blue('\n๐Ÿ“Š Analysis Summary:')); console.log(` Repository: ${analysis.repository.full_name}`); console.log(` Files analyzed: ${analysis.source_files.length}/${analysis.file_tree.total_files}`); console.log(` Languages: ${analysis.analysis_summary.languages.join(', ')}`); console.log(` Total lines: ${analysis.analysis_summary.total_lines.toLocaleString()}`); config_1.default.set('last_analysis_id', savedAnalysis.analysis_id); config_1.default.set('last_repo_name', repoName); const analysisDataPath = path_1.default.join(process.cwd(), '.doclyft-analysis.json'); await fs_1.promises.writeFile(analysisDataPath, JSON.stringify(analysis, null, 2)); console.log(chalk_1.default.green('\nโœ… Analysis complete! Next steps:')); console.log(chalk_1.default.cyan(' /generate readme - Generate README')); console.log(chalk_1.default.cyan(' /generate docs - Generate full documentation')); } catch (error) { spinner.fail(chalk_1.default.red(`Analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`)); } } async analyzeList() { await auth_1.default.requireAuth(); const spinner = (0, ora_1.default)('Fetching analyzed repositories...').start(); try { const repos = await api_1.default.listAnalyzedRepos(); spinner.stop(); if (repos.length === 0) { console.log(chalk_1.default.yellow('No repositories analyzed yet.')); console.log(chalk_1.default.blue('๐Ÿ’ก Use /analyze repo owner/name to analyze a repository')); return; } console.log(chalk_1.default.blue(`\n๐Ÿ“š Found ${repos.length} analyzed repositories:\n`)); repos.forEach((repo, index) => { const lastAnalyzed = new Date(repo.created_at).toLocaleDateString(); console.log(chalk_1.default.white(`${index + 1}. ${chalk_1.default.bold(repo.repo_full_name)}`)); console.log(chalk_1.default.gray(` Languages: ${repo.languages?.join(', ') || 'N/A'}`)); console.log(chalk_1.default.gray(` Files: ${repo.total_files || 'N/A'} | Lines: ${repo.total_lines?.toLocaleString() || 'N/A'}`)); console.log(chalk_1.default.gray(` Last analyzed: ${lastAnalyzed}`)); console.log(''); }); console.log(chalk_1.default.blue('๐Ÿ’ก Use /analyze select to select one for documentation generation')); } catch (error) { spinner.fail(chalk_1.default.red(`Failed to fetch repositories: ${error instanceof Error ? error.message : 'Unknown error'}`)); } } async analyzeSelect() { await auth_1.default.requireAuth(); const spinner = (0, ora_1.default)('Fetching analyzed repositories...').start(); const repos = await api_1.default.listAnalyzedRepos(); spinner.stop(); if (repos.length === 0) { console.log(chalk_1.default.yellow('No repositories analyzed yet.')); return; } const choices = repos.map((repo) => ({ title: `${repo.repo_full_name}`, description: `${repo.languages?.join(', ') || 'N/A'} | ${repo.total_files || 'N/A'} files | ${new Date(repo.created_at).toLocaleDateString()}`, value: repo })); const response = await (0, prompts_1.default)({ type: 'select', name: 'selectedRepo', message: 'Select a repository:', choices, initial: 0 }); if (!response.selectedRepo) return; const selectedRepo = response.selectedRepo; const spinner2 = (0, ora_1.default)('Loading repository analysis...').start(); try { const analysisData = await api_1.default.getAnalysisData(selectedRepo.id); const analysisDataPath = path_1.default.join(process.cwd(), '.doclyft-analysis.json'); await fs_1.promises.writeFile(analysisDataPath, JSON.stringify(analysisData, null, 2)); config_1.default.set('last_analysis_id', selectedRepo.id); config_1.default.set('last_repo_name', selectedRepo.repo_full_name); spinner2.succeed(chalk_1.default.green(`Repository ${selectedRepo.repo_full_name} loaded!`)); console.log(chalk_1.default.green('\nโœ… Ready for documentation generation:')); console.log(chalk_1.default.cyan(' /generate readme')); console.log(chalk_1.default.cyan(' /generate docs')); } catch (error) { spinner2.fail(chalk_1.default.red(`Failed to load repository: ${error instanceof Error ? error.message : 'Unknown error'}`)); } } /** * Handle generate command */ async handleGenerate(args) { await auth_1.default.requireAuth(); const subcommand = args[0] || 'readme'; switch (subcommand) { case 'readme': await this.generateReadme(); break; case 'docs': await this.generateDocs(); break; case 'list': await this.generateList(); break; default: console.log(chalk_1.default.red(`โŒ Unknown generate subcommand: ${subcommand}`)); console.log(chalk_1.default.blue('๐Ÿ’ก Available: readme, docs, list')); } } async generateReadme() { const analysisDataPath = path_1.default.join(process.cwd(), '.doclyft-analysis.json'); let analysisData; try { const analysisDataStr = await fs_1.promises.readFile(analysisDataPath, 'utf-8'); analysisData = JSON.parse(analysisDataStr); } catch (error) { console.log(chalk_1.default.red('โŒ No analysis data found. Use /analyze repo first.')); return; } const outputFile = 'README.md'; // Check if file exists try { await fs_1.promises.access(outputFile); const confirm = await (0, prompts_1.default)({ type: 'confirm', name: 'overwrite', message: `${outputFile} already exists. Overwrite?`, initial: false }); if (!confirm.overwrite) { console.log(chalk_1.default.yellow('Cancelled.')); return; } } catch { // File doesn't exist, proceed } const { user_id, user_email } = auth_1.default.getAuthInfo(); const spinner = (0, ora_1.default)('Generating README with AI...').start(); try { const saveResult = await api_1.default.saveAnalysis(analysisData, user_id, user_email); const readmeContent = await api_1.default.generateReadme(saveResult.analysis_id); await fs_1.promises.writeFile(outputFile, readmeContent); spinner.succeed(chalk_1.default.green(`Successfully generated ${outputFile}`)); console.log(chalk_1.default.blue(`๐Ÿ“„ README generated for ${analysisData.repository?.full_name || 'repository'}`)); console.log(chalk_1.default.green('\nโœ… Next steps:')); console.log(chalk_1.default.cyan(' /push - Push to GitHub')); } catch (error) { spinner.fail(chalk_1.default.red(`README generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`)); } } async generateDocs() { const analysisDataPath = path_1.default.join(process.cwd(), '.doclyft-analysis.json'); let analysisData; try { const analysisDataStr = await fs_1.promises.readFile(analysisDataPath, 'utf-8'); analysisData = JSON.parse(analysisDataStr); } catch (error) { console.log(chalk_1.default.red('โŒ No analysis data found. Use /analyze repo first.')); return; } const { user_id, user_email } = auth_1.default.getAuthInfo(); const outputDir = 'docs'; const spinner = (0, ora_1.default)('Generating documentation with AI...').start(); try { const result = await api_1.default.generateDocs(analysisData, user_id, user_email); await fs_1.promises.mkdir(outputDir, { recursive: true }); for (const [sectionName, sectionData] of Object.entries(result.documentation)) { const filename = `${sectionName}.md`; const filepath = path_1.default.join(outputDir, filename); await fs_1.promises.writeFile(filepath, sectionData.content); } const indexContent = `# Documentation Index\n\n> Generated by DocLyft AI\n\n` + Object.entries(result.documentation).map(([section, data]) => `- [${data.title || section.charAt(0).toUpperCase() + section.slice(1)}](${section}.md)`).join('\n'); await fs_1.promises.writeFile(path_1.default.join(outputDir, 'README.md'), indexContent); spinner.succeed(chalk_1.default.green(`Successfully generated documentation in ${outputDir}`)); console.log(chalk_1.default.blue('\n๐Ÿ“š Documentation generated:')); console.log(` Sections: ${result.sections_generated.join(', ')}`); console.log(` Files created: ${Object.keys(result.documentation).length + 1}`); console.log(chalk_1.default.green('\nโœ… Use /push to push to GitHub')); } catch (error) { spinner.fail(chalk_1.default.red(`Documentation generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`)); } } async generateList() { // Implementation for listing generated READMEs console.log(chalk_1.default.blue('๐Ÿ“„ Searching for README files...')); // Add implementation similar to the original CLI } /** * Handle push command */ async handlePush(args) { await auth_1.default.requireAuth(); console.log(chalk_1.default.blue('๐ŸŽฏ Interactive Push Mode\n')); try { let readmeContent; let targetRepo; let readmeFile; // Step 1: Select README file const currentDir = process.cwd(); const readmeFiles = []; console.log(chalk_1.default.blue('๐Ÿ“„ Scanning for README files...')); const checkDirectory = async (dir, depth = 0) => { if (depth > 2) return; try { const entries = await fs_1.promises.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path_1.default.join(dir, entry.name); const relativePath = path_1.default.relative(currentDir, fullPath); if (entry.isFile() && /^readme/i.test(entry.name) && entry.name.endsWith('.md')) { const stats = await fs_1.promises.stat(fullPath); readmeFiles.push({ path: relativePath, name: entry.name, size: stats.size, modified: stats.mtime }); } else if (entry.isDirectory() && !entry.name.startsWith('.') && !['node_modules', 'dist', 'build'].includes(entry.name)) { await checkDirectory(fullPath, depth + 1); } } } catch (error) { // Silently skip directories we can't read } }; await checkDirectory(currentDir); if (readmeFiles.length === 0) { console.log(chalk_1.default.red('โŒ No README files found in current directory.')); console.log(chalk_1.default.blue('\n๐Ÿ’ก Generate a README first using:')); console.log(chalk_1.default.cyan(' /generate readme')); return; } // Sort by modification date (newest first) readmeFiles.sort((a, b) => b.modified.getTime() - a.modified.getTime()); const readmeChoices = readmeFiles.map((file) => { const sizeKB = (file.size / 1024).toFixed(1); const modifiedTime = file.modified.toLocaleDateString(); return { title: file.path, description: `${sizeKB} KB | Modified: ${modifiedTime}`, value: file.path }; }); const readmeResponse = await (0, prompts_1.default)({ type: 'select', name: 'selectedFile', message: 'Select README file to push:', choices: readmeChoices, initial: 0 }); if (!readmeResponse.selectedFile) { console.log(chalk_1.default.yellow('No README file selected.')); return; } readmeFile = readmeResponse.selectedFile; // Step 2: Select target repository console.log(chalk_1.default.blue('\n๐Ÿ“ก Fetching your GitHub repositories...')); const spinner = (0, ora_1.default)('Loading repositories...').start(); try { // Try to detect git remotes from current directory first const localRepos = []; try { const { exec } = await Promise.resolve().then(() => __importStar(require('child_process'))); const { promisify } = await Promise.resolve().then(() => __importStar(require('util'))); const execAsync = promisify(exec); // List all git remotes const { stdout } = await execAsync('git remote -v', { cwd: process.cwd(), timeout: 5000 }); // Parse output to extract repository names const remoteRegex = /^(\S+)\s+(?:https:\/\/github\.com\/|git@github\.com:)([^/]+\/[^.]+)(?:\.git)?\s+\(push\)$/gm; let match; const seenRepos = new Set(); while ((match = remoteRegex.exec(stdout)) !== null) { const repoFullName = match[2].trim(); if (!seenRepos.has(repoFullName)) { localRepos.push({ name: repoFullName, url: `https://github.com/${repoFullName}` }); seenRepos.add(repoFullName); } } if (localRepos.length > 0) { console.log(chalk_1.default.green(`\nโœ… Found ${localRepos.length} Git remote(s) in current directory`)); } } catch (gitError) { // Silently handle git command errors } // Now fetch user's GitHub repos const userRepos = await api_1.default.getGitHubRepos(); spinner.stop(); // Combine local and user repos, prioritizing local ones const localRepoNames = new Set(localRepos.map(r => r.name)); const combinedRepos = [ ...localRepos.map(repo => ({ full_name: repo.name, description: '๐Ÿ–ฅ๏ธ Local Git remote', private: false, html_url: repo.url, isLocal: true })), ...userRepos.filter(repo => !localRepoNames.has(repo.full_name)) ]; if (combinedRepos.length === 0) { console.log(chalk_1.default.yellow('No repositories found in your GitHub account or local Git config.')); // Prompt for manual entry const manualEntry = await (0, prompts_1.default)({ type: 'text', name: 'repoName', message: 'Enter target repository (owner/name):', validate: (value) => { if (!value.includes('/')) { return 'Repository must be in format owner/name'; } return true; } }); if (!manualEntry.repoName) { console.log(chalk_1.default.yellow('No repository specified.')); return; } targetRepo = manualEntry.repoName; } else { // Build repo choices, highlighting local repos const repoChoices = combinedRepos.slice(0, 50).map(repo => ({ title: repo.full_name, description: 'isLocal' in repo && repo.isLocal ? '๐Ÿ–ฅ๏ธ Local Git remote' : `${repo.private ? '๐Ÿ”’ Private' : '๐ŸŒ Public'} | ${repo.description || 'No description'}`, value: repo.full_name })); const repoResponse = await (0, prompts_1.default)({ type: 'select', name: 'selectedRepo', message: 'Choose the repository you want to push to:', choices: repoChoices, initial: 0 }); if (!repoResponse.selectedRepo) { console.log(chalk_1.default.yellow('No repository selected.')); return; } targetRepo = repoResponse.selectedRepo; } // Read the selected README content if (readmeFile) { try { readmeContent = await fs_1.promises.readFile(readmeFile, 'utf-8'); console.log(chalk_1.default.green(`\nโœ… Selected README: ${readmeFile}`)); console.log(chalk_1.default.green(`โœ… Target repository: ${targetRepo}`)); } catch (error) { console.log(chalk_1.default.red(`โŒ Failed to read README file: ${error instanceof Error ? error.message : 'Unknown error'}`)); return; } } } catch (error) { spinner.fail('Failed to load repositories'); console.log(chalk_1.default.red(`โŒ Error fetching repositories: ${error instanceof Error ? error.message : 'Unknown error'}`)); return; } // Step 3: Get analysis data const analysisDataPath = path_1.default.join(process.cwd(), '.doclyft-analysis.json'); let analysisData; try { const analysisDataStr = await fs_1.promises.readFile(analysisDataPath, 'utf-8'); analysisData = JSON.parse(analysisDataStr); console.log(chalk_1.default.blue('\n๐Ÿ“Š Using local analysis data')); } catch (error) { // If no local data, try to get it from the backend const analysisId = config_1.default.get('last_analysis_id'); if (!analysisId) { console.log(chalk_1.default.red('\nโŒ No analysis data found. Please run:')); console.log(chalk_1.default.cyan(' /analyze repo owner/name')); return; } console.log(chalk_1.default.blue('\n๐Ÿ“ก Fetching analysis data from server...')); const spinner2 = (0, ora_1.default)('Loading analysis data...').start(); try { analysisData = await api_1.default.getAnalysisData(analysisId); // Save it locally for future use await fs_1.promises.writeFile(analysisDataPath, JSON.stringify(analysisData, null, 2)); spinner2.succeed('Analysis data loaded'); } catch (fetchError) { spinner2.fail('Failed to load analysis data'); throw fetchError; } } // Override repository if specified if (targetRepo && analysisData.repository) { analysisData.repository.full_name = targetRepo; const [owner, name] = targetRepo.split('/'); analysisData.repository.owner = owner; analysisData.repository.name = name; } // If we have custom README content, use it if (readmeContent) { if (!analysisData.documentation) { analysisData.documentation = {}; } analysisData.documentation.readme = readmeContent; } // Step 4: Push to GitHub const pushOptions = { export_type: 'readme', commit_message: 'Update documentation via DocLyft CLI' }; const pushSpinner = (0, ora_1.default)('Pushing documentation to GitHub...').start(); try { const pushResult = await api_1.default.pushDocs(analysisData, pushOptions); pushSpinner.succeed(chalk_1.default.green('Successfully pushed documentation to GitHub!')); console.log(chalk_1.default.blue(`\n๐Ÿš€ Documentation pushed to ${targetRepo}`)); console.log(` Source file: ${readmeFile}`); console.log(` Type: README`); // Log the push activity try { await this.activityLogger.init(); const { user_id } = auth_1.default.getAuthInfo(); await this.activityLogger.logActivity({ type: 'push', timestamp: new Date().toISOString(), repo: targetRepo, files: readmeFile ? [readmeFile] : undefined, target: 'main', analysis_id: analysisData.id || config_1.default.get('last_analysis_id'), user_id: user_id || undefined }); // Sync activities to backend try { await this.activityLogger.syncLogsToBackend(); } catch (syncError) { console.log(chalk_1.default.yellow('โš ๏ธ Warning: Could not sync activities to backend')); } } catch (logError) { // Silently handle logging errors } console.log(chalk_1.default.green('\nโœ… Push completed successfully!')); } catch (pushError) { pushSpinner.fail('Push failed'); throw pushError; } } catch (error) { console.log(chalk_1.default.red(`โŒ Push failed: ${error instanceof Error ? error.message : 'Unknown error'}`)); // Provide specific guidance for common issues if (error instanceof Error && error.message.includes('GitHub token')) { console.log(chalk_1.default.yellow('\n๐Ÿ’ก To fix this:')); console.log(chalk_1.default.cyan(' 1. Use /github-token to set up GitHub access')); console.log(chalk_1.default.cyan(' 2. Make sure the token has "repo" permissions')); } } } /** * Handle repos command */ async handleRepos(args) { await auth_1.default.requireAuth(); const spinner = (0, ora_1.default)('Fetching your GitHub repositories...').start(); try { const repos = await api_1.default.getGitHubRepos(); spinner.stop(); if (repos.length === 0) { console.log(chalk_1.default.yellow('No repositories found in your GitHub account.')); return; } const limit = 25; const displayRepos = repos.slice(0, limit); console.log(chalk_1.default.blue(`\n๐Ÿ“š Found ${repos.length} repositories (showing ${displayRepos.length}):\n`)); displayRepos.forEach((repo, index) => { console.log(chalk_1.default.white(`${index + 1}. ${chalk_1.default.bold(repo.full_name)}`)); console.log(chalk_1.default.gray(` ${repo.private ? '๐Ÿ”’ Private' : '๐ŸŒ Public'} | ${repo.description || 'No description'}`)); console.log(''); }); if (repos.length > limit) { console.log(chalk_1.default.gray(`... and ${repos.length - limit} more`)); } console.log(chalk_1.default.green('\nโœ… To analyze a repository:')); console.log(chalk_1.default.cyan(' /analyze repo owner/name')); } catch (error) { spinner.fail(chalk_1.default.red(`Failed to fetch repositories: ${error instanceof Error ? error.message : 'Unknown error'}`)); } } /** * Handle history command */ async handleHistory(args) { try { await this.activityLogger.init(); const limit = 20; const activities = await this.activityLogger.getActivities(limit); if (activities.length === 0) { console.log(chalk_1.default.yellow('No activities found in the history.')); return; } console.log(chalk_1.default.blue(`\n๐Ÿ“œ Showing last ${activities.length} activities:\n`)); activities.reverse().forEach((activity) => { const date = new Date(activity.timestamp).toLocaleString(); const eventType = activity.type.toUpperCase(); let color; switch (activity.type) { case 'generate': color = chalk_1.default.green; break; case 'edit': color = chalk_1.default.blue; break; case 'push': color = chalk_1.default.magenta; break; case 'analyze': color = chalk_1.default.cyan; break; default: color = chalk_1.default.white; } console.log(color(`[${eventType}] ${date}`)); if (activity.repo) { console.log(` Repository: ${activity.repo}`); } if (activity.files && activity.files.length > 0) { console.log(` Files: ${activity.files.join(', ')}`); } console.log(''); }); } catch (error) { console.log(chalk_1.default.red(`โŒ Failed to load activity history: ${error instanceof Error ? error.message : 'Unknown error'}`)); } } /** * Handle config command */ async handleConfig(args) { const subcommand = args[0]; switch (subcommand) { case 'list': this.configList(); break; case 'set': if (args.length < 3) { console.log(chalk_1.default.red('โŒ Usage: /config set <key> <value>')); return; } this.configSet(args[1], args[2]); break; case 'get': if (args.length < 2) { console.log(chalk_1.default.red('โŒ Usage: /config get <key>')); return; } this.configGet(args[1]); break; case 'clear': await this.configClear(); break; default: console.log(chalk_1.default.red(`โŒ Unknown config subcommand: ${subcommand}`)); console.log(chalk_1.default.blue('๐Ÿ’ก Available: list, set, get, clear')); } } configList() { const config = config_1.default.getAll(); if (Object.keys(config).length === 0) { console.log(chalk_1.default.yellow('No configuration found.')); } else { console.log(chalk_1.default.blue('Current configuration:')); for (const [key, value] of Object.entries(config)) { const displayValue = key.includes('token') ? '***hidden***' : value; console.log(` ${key}: ${displayValue}`); } } } configSet(key, value) { const validKeys = ['token', 'repo', 'branch', 'user_id', 'user_email', 'github_token', 'last_analysis_id', 'last_repo_name']; if (!validKeys.includes(key)) { console.log(chalk_1.default.red(`Invalid key '${key}'. Valid keys: ${validKeys.join(', ')}`)); return; } config_1.default.set(key, value); console.log(chalk_1.default.green(`โœ… Set '${key}' to '${key.includes('token') ? '***hidden***' : value}'`)); } configGet(key) { const validKeys = ['token', 'repo', 'branch', 'user_id', 'user_email', 'github_token', 'last_analysis_id', 'last_repo_name']; if (!validKeys.includes(key)) { console.log(chalk_1.default.red(`Invalid key '${key}'. Valid keys: ${validKeys.join(', ')}`)); return; } const value = config_1.default.get(key); if (value) { console.log(key.includes('token') ? '***hidden***' : value); } else { console.log(chalk_1.default.yellow(`Key '${key}' not found.`)); } } async configClear() { const confirm = await (0, prompts_1.default)({ type: 'confirm', name: 'clear', message: 'This will remove all stored configuration. Continue?', initial: false }); if (confirm.clear) { config_1.default.clear(); console.log(chalk_1.default.green('โœ… Configuration cleared.')); } } /** * Handle test command */ async handleTest() { console.log(chalk_1.default.blue('๐Ÿงช Testing GitHub connectivity...')); const githubToken = config_1.default.get('github_token'); if (!githubToken) { console.log(chalk_1.default.red('โŒ No GitHub token configured')); console.log(chalk_1.default.yellow('๐Ÿ’ก Use /github-token to set up GitHub access')); return; } console.log(chalk_1.default.green('โœ… GitHub token found')); const spinner = (0, ora_1.default)('Testing GitHub API connectivity...').start(); try { const axios = (await Promise.resolve().then(() => __importStar(require('axios')))).default; const response = await axios.get('https://api.github.com/user', { headers: { 'Authorization': `Bearer ${githubToken}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'DocLyft-CLI/1.0' }, timeout: 10000 }); spinner.succeed('GitHub API connectivity successful'); const user = response.data; console.log(chalk_1.default.blue(`\n๐Ÿ‘ค Connected as: ${user.login}`)); console.log(chalk_1.default.blue(`๐Ÿ“ง Email: ${user.email || 'Not public'}`)); console.log(chalk_1.default.blue(`๐Ÿ“Š Public repos: ${user.public_repos}`)); console.log(chalk_1.default.green('\n๐ŸŽ‰ GitHub integration is ready!')); } catch (error) { spinner.fail('GitHub API test failed'); console.log(chalk_1.default.red(`โŒ Error: ${error instanceof Error ? error.message : 'Unknown error'}`)); console.log(chalk_1.default.yellow('๐Ÿ’ก Use /github-token to set up a new token')); } } /** * Handle github-token command */ async handleGitHubToken() { console.log(chalk_1.default.blue('๐Ÿ” GitHub Token Configuration')); console.log(chalk_1.default.yellow('You need a GitHub Personal Access Token to push documentation.')); console.log(chalk_1.default.blue('Create one at: https://github.com/settings/personal-access-tokens/new\n')); const response = await (0, prompts_1.default)({ type: 'password', name: 'token', message: 'Enter your GitHub personal access token:', }); if (!response.token) { console.log(chalk_1.defaul