UNPKG

doclyft

Version:

CLI for DocLyft - Interactive documentation generator with hosted documentation support

1,095 lines (1,094 loc) β€’ 151 kB
#!/usr/bin/env node "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 commander_1 = require("commander"); const prompts_1 = __importDefault(require("prompts")); const config_1 = __importDefault(require("./services/config")); const api_1 = __importDefault(require("./services/api")); const activity_logger_1 = require("./utils/activity-logger"); const command_protection_1 = require("./utils/command-protection"); const auth_1 = __importStar(require("./middleware/auth")); const banner_1 = require("./utils/banner"); const version_check_1 = require("./utils/version-check"); const visual_1 = require("./utils/visual"); const ui_patterns_1 = require("./utils/ui-patterns"); const ora_1 = __importDefault(require("ora")); const chalk_1 = __importDefault(require("chalk")); const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const crypto_1 = __importDefault(require("crypto")); const packageJson = require('../package.json'); const program = new commander_1.Command(); program .name('doclyft') .description('CLI for DocLyft - AI-Powered Documentation Generator') .version('1.6.1'); program .command('login') .description('Authenticate with your DocLyft API token') .option('--interactive', 'Start interactive session after login') .action(async (options) => { (0, ui_patterns_1.initCommand)('πŸ” DocLyft Authentication'); // Instructions box const instructionsContent = [ `${chalk_1.default.cyan('πŸ“‹ How to get your API token:')}`, '', `1. Visit ${chalk_1.default.underline('https://doclyft.com/dashboard/api-keys')}`, `2. Generate or copy your API token`, `3. Paste it below when prompted`, '', `${chalk_1.default.gray('Your token should start with "dk_prod_" and be secure!')}` ]; console.log((0, visual_1.createBox)(instructionsContent, { title: 'πŸ“– Setup Instructions', style: 'single', color: 'blue', padding: 0, width: 65 })); console.log('\n'); const { token } = await (0, prompts_1.default)({ type: 'password', name: 'token', message: `${chalk_1.default.blue('πŸ”‘')} Enter your DocLyft API token:`, }); if (!token) { console.log((0, visual_1.createErrorMessage)('Authentication cancelled', ['Token is required to proceed'])); return; } // Trim whitespace from token const cleanToken = token.trim(); // Basic token format validation if (!cleanToken.startsWith('dk_prod_')) { console.log((0, visual_1.createErrorMessage)('Invalid token format', [ 'DocLyft API tokens must start with "dk_prod_"', 'Get your API token from: https://doclyft.com/dashboard/api-keys', 'Make sure you copied the complete token' ])); return; } if (cleanToken.length < 20) { console.log((0, visual_1.createErrorMessage)('Incomplete token detected', [ 'The token appears to be cut off or incomplete', 'Make sure you copied the full token from the platform', 'Tokens are typically 40+ characters long' ])); return; } console.log('\n'); console.log((0, visual_1.createDivider)('Verification', { char: '─', color: 'blue', width: 50 })); const spinner = (0, ora_1.default)('πŸ” Verifying token with DocLyft servers...').start(); try { const isValid = await api_1.default.verifyToken(cleanToken); if (!isValid) { spinner.fail(); console.log((0, visual_1.createErrorMessage)('Token verification failed', [ 'Your API token could not be verified', 'Ensure your token starts with "dk_prod_"', 'Check that you copied the complete token', 'Verify your internet connection', 'Try generating a new token if issues persist' ])); return; } spinner.text = 'πŸ‘€ Fetching user information...'; // Get user info for the session const userInfo = await api_1.default.getUserInfo(cleanToken); spinner.text = 'πŸ’Ύ Saving authentication...'; // Store in config 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 with the SessionManager const SessionManager = (await Promise.resolve().then(() => __importStar(require('./services/session')))).default; await SessionManager.createSession(cleanToken, userInfo.user_id, userInfo.user_email); spinner.succeed(); // Success message console.log((0, visual_1.createSuccessMessage)('Authentication successful!', [ `Logged in as: ${userInfo.user_email}`, 'Session saved securely with encryption', 'Ready to use DocLyft CLI commands' ])); // Next steps const nextStepsContent = [ `${chalk_1.default.green('🎯 What\'s next?')}`, '', options.interactive ? `${chalk_1.default.cyan('Starting interactive session...')}` : `${chalk_1.default.blue('Quick start options:')}`, options.interactive ? '' : ` β€’ ${chalk_1.default.green('doclyft interactive')} - Enter interactive mode`, options.interactive ? '' : ` β€’ ${chalk_1.default.green('doclyft status')} - View current status`, options.interactive ? '' : ` β€’ ${chalk_1.default.green('doclyft analyze repo --repo owner/name')} - Analyze a repository` ].filter(line => line !== ''); console.log((0, visual_1.createBox)(nextStepsContent, { title: 'πŸš€ Ready to Go', style: 'single', color: 'green', padding: 0, width: 60 })); if (options.interactive) { console.log('\n'); console.log((0, visual_1.createDivider)('Interactive Session', { char: '═', color: 'green', width: 50 })); const { default: InteractiveSession } = await Promise.resolve().then(() => __importStar(require('./services/interactive-session'))); const session = new InteractiveSession(); await session.start({ showWelcome: false }); } } catch (error) { spinner.fail(); if (error instanceof Error && error.message.includes('timeout')) { console.log((0, visual_1.createErrorMessage)('Connection timeout', [ 'Failed to connect to DocLyft servers', 'Check your internet connection', 'Try again in a few moments', 'Contact support if the issue persists' ])); } else { console.log((0, visual_1.createErrorMessage)('Login failed', [ error instanceof Error ? error.message : 'Unknown error occurred', 'Check your internet connection', 'Verify your token is correct', 'Try again or contact support' ])); } } console.log('\n'); }); program .command('logout') .description('Remove stored authentication') .action(async () => { (0, ui_patterns_1.initCommand)('πŸšͺ Logout', { width: 50 }); const spinner = (0, ora_1.default)('πŸ” Clearing authentication data...').start(); try { // Clear from config config_1.default.delete('token'); config_1.default.delete('user_id'); config_1.default.delete('user_email'); config_1.default.delete('github_token'); // Clear session using SessionManager const SessionManager = (await Promise.resolve().then(() => __importStar(require('./services/session')))).default; await SessionManager.clearSession(); spinner.succeed(); console.log((0, visual_1.createSuccessMessage)('Successfully logged out!', [ 'All authentication data cleared', 'Session data securely removed', 'Run "doclyft login" to authenticate again' ])); } catch (error) { spinner.warn('Partial logout completed'); console.log((0, visual_1.createSuccessMessage)('Logout completed with warnings', [ 'Authentication data cleared', 'Some session files may remain', 'Run "doclyft login" to authenticate again' ])); } console.log('\n'); }); // Add interactive command program .command('interactive') .alias('i') .description('Start interactive CLI session with slash commands') .action(async () => { try { const { default: InteractiveSession } = await Promise.resolve().then(() => __importStar(require('./services/interactive-session'))); const session = new InteractiveSession(); await session.start({ showWelcome: true, autoLogin: false }); } catch (error) { console.log(chalk_1.default.red(`❌ Failed to start interactive session: ${error instanceof Error ? error.message : 'Unknown error'}`)); } }); // Add analyze command to match web app workflow const analyze = program.command('analyze') .description('Analyze a GitHub repository'); const analyzeRepo = analyze .command('repo') .description('Analyze a GitHub repository') .option('--repo <owner/name>', 'Repository in format owner/name (e.g., facebook/react)') .option('--token <token>', 'GitHub personal access token') .option('--branch <branch>', 'Default branch (auto-detected if not specified)') .action(async (options) => { (0, ui_patterns_1.initCommand)('πŸ” Repository Analysis'); // Show current working directory confirmation (0, ui_patterns_1.showWorkingDirectoryConfirmation)('analysis'); // Ask for confirmation const dirConfirm = await (0, prompts_1.default)({ type: 'confirm', name: 'continue', message: 'Continue with analysis in this directory?', initial: true }); if (!dirConfirm.continue) { console.log(''); console.log((0, visual_1.createStatus)('info', 'Analysis cancelled by user')); console.log(''); console.log((0, visual_1.createBox)([ 'πŸ“ Change Directory Instructions:', '', '1. Use "cd" to navigate to your project directory', '2. Run the analysis command again', '3. Example: cd /path/to/your/project && doclyft analyze repo', '', 'πŸ’‘ Make sure you\'re in the root of your project!' ], { title: 'πŸ—ΊοΈ Navigation Help', style: 'single', color: 'blue', padding: 1, width: 65 })); return; } // Get authenticated user info (guaranteed to exist due to auth guard) const { user_id, user_email } = (0, command_protection_1.getRequiredAuthInfo)(); // Initialize activity logger for this session const activityLogger = new activity_logger_1.ActivityLogger(); await activityLogger.init(); const sessionId = crypto_1.default.randomUUID(); try { await activityLogger.logActivity({ type: 'analyze', timestamp: new Date().toISOString(), repo: options.repo, user_id: user_id }); let repoName = options.repo; let githubToken = options.token; console.log((0, visual_1.createBox)([ 'πŸ“‹ Repository Analysis Requirements:', '', 'β€’ Repository name in format: owner/name (e.g., facebook/react)', 'β€’ GitHub Personal Access Token with repo permissions', 'β€’ Valid repository access (public or token has permissions)', '', 'πŸ’‘ Create token at: https://github.com/settings/personal-access-tokens/new' ], { title: 'πŸ“– Getting Started', style: 'single', color: 'blue', padding: 1, width: 75 })); console.log(''); // Prompt for missing parameters if (!repoName) { console.log((0, visual_1.createStatus)('info', 'Repository name required')); 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; } console.log((0, visual_1.createDivider)('Repository Validation', { color: 'blue', width: 70 })); // Check if this is a team repository first let isTeamRepo = false; const teamSpinner = (0, ora_1.default)('πŸ” Checking team repository access...').start(); try { const teamRepos = await api_1.default.getTeamRepositories(); isTeamRepo = teamRepos.some(repo => repo.full_name === repoName); if (isTeamRepo) { teamSpinner.succeed(); console.log((0, visual_1.createStatus)('success', `Repository ${repoName} is a team repository - using team owner's GitHub token`)); } else { teamSpinner.info('Repository not found in team repositories - using personal token'); } } catch (error) { teamSpinner.warn('Could not check team repositories - continuing with personal flow'); // Continue with personal repo flow if team check fails } if (!githubToken && !isTeamRepo) { // First check if we have a token from any source const tokenStatus = await api_1.default.hasGitHubToken(); if (tokenStatus.hasToken) { try { // Get the token (this will fetch from backend if needed) 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 we still don't have a token, prompt the user if (!githubToken) { console.log(''); console.log((0, visual_1.createStatus)('warning', 'No GitHub token found in configuration')); console.log((0, visual_1.createBox)([ 'πŸ”‘ GitHub Token Required', '', 'A GitHub Personal Access Token is needed to analyze repositories.', 'The token should have the following permissions:', '', 'β€’ repo (for private repository access)', 'β€’ public_repo (for public repository access)', 'β€’ read:org (if analyzing organization repositories)', '', 'πŸ”— Create token at: https://github.com/settings/personal-access-tokens/new' ], { title: '⚠️ Authentication Required', style: 'single', color: 'yellow', padding: 1, width: 75 })); console.log(''); const response = await (0, prompts_1.default)({ type: 'password', name: 'token', message: 'Enter GitHub personal access token:', }); githubToken = response.token; if (githubToken) { // Validate the token format try { const { validateGitHubToken } = await Promise.resolve().then(() => __importStar(require('./utils/validation'))); if (githubToken) { validateGitHubToken(githubToken); } else { throw new Error('GitHub token is required'); } // Ask if they want to store it const store = await (0, prompts_1.default)({ type: 'confirm', name: 'store', message: 'Store GitHub token for future use?', initial: true }); if (store.store) { config_1.default.set('github_token', githubToken); console.log((0, visual_1.createStatus)('success', 'GitHub token stored for future use')); } } catch (validationError) { console.log(''); console.log((0, visual_1.createErrorMessage)('Invalid GitHub token format', [ validationError instanceof Error ? validationError.message : 'Unknown error', 'Make sure you\'re using a GitHub Personal Access Token', 'Not a DocLyft API key or other token type' ])); return; } } } else { // Validate existing token format try { const { validateGitHubToken } = await Promise.resolve().then(() => __importStar(require('./utils/validation'))); validateGitHubToken(githubToken); } catch (validationError) { console.log(''); console.log((0, visual_1.createErrorMessage)('Stored GitHub token is invalid', [ validationError instanceof Error ? validationError.message : 'Unknown error', 'Please set a new valid GitHub token' ])); console.log((0, visual_1.createBox)([ 'πŸ”§ Token Update Instructions:', '', '1. Create new token: https://github.com/settings/personal-access-tokens/new', '2. Update CLI configuration:', ' doclyft config set github_token <your_new_token>', '3. Try the analysis again' ], { title: 'πŸ’‘ Quick Fix', style: 'single', color: 'cyan', padding: 1, width: 70 })); return; } } } if (!repoName || (!githubToken && !isTeamRepo)) { console.log(''); console.log((0, visual_1.createErrorMessage)('Missing required parameters', [ 'Repository name is required', 'GitHub token is required (unless team repository)', 'Run the command again with proper parameters' ])); return; } const [owner, name] = repoName.split('/'); console.log(''); console.log((0, visual_1.createDivider)('Analysis Preparation', { color: 'blue', width: 70 })); // Auto-detect default branch if not specified let defaultBranch = options.branch; if (!defaultBranch) { const branchSpinner = (0, ora_1.default)('πŸ” Auto-detecting default branch...').start(); try { const repoInfo = await api_1.default.getRepositoryInfo(repoName); defaultBranch = repoInfo.default_branch; branchSpinner.succeed(`Detected default branch: ${defaultBranch}`); } catch (error) { branchSpinner.warn('Could not detect default branch, using "main"'); defaultBranch = 'main'; } } console.log(''); console.log((0, visual_1.createDivider)('Repository Analysis', { color: 'green', width: 70 })); const analysisSpinner = (0, ora_1.default)('πŸ”¬ Analyzing repository structure...').start(); 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 || 'main', team_analysis: isTeamRepo }); analysisSpinner.text = 'πŸ’Ύ Saving analysis results...'; // Enhance analysis data with CLI context for backend sync const enhancedAnalysis = { ...analysis, cli_session_id: sessionId, origin_source: 'cli', cli_context: { version: '1.6.1', command: 'analyze repo', args: [options.repo, `--branch=${options.branch}`].filter(Boolean) } }; const savedAnalysis = await api_1.default.saveAnalysis(enhancedAnalysis, user_id, user_email); // Log the successful analysis await activityLogger.logActivity({ type: 'analyze', timestamp: new Date().toISOString(), repo: repoName, analysis_id: savedAnalysis.analysis_id, user_id: user_id }); // Sync activities to backend try { await activityLogger.syncLogsToBackend(); } catch (syncError) { console.log((0, visual_1.createStatus)('warning', 'Could not sync activity to backend')); } analysisSpinner.succeed('Repository analyzed successfully!'); console.log(''); console.log((0, visual_1.createDivider)('Analysis Results', { color: 'green', width: 70 })); // Create analysis summary table const summaryData = [ { key: 'πŸ“ Repository', value: analysis.repository.full_name }, { key: 'πŸ“„ Files Analyzed', value: `${analysis.source_files.length}/${analysis.file_tree.total_files}` }, { key: 'πŸ’» Languages', value: analysis.analysis_summary.languages.join(', ') }, { key: 'πŸ“Š Total Lines', value: analysis.analysis_summary.total_lines.toLocaleString() }, { key: 'πŸ†” Analysis ID', value: savedAnalysis.analysis_id }, { key: '🌿 Branch', value: defaultBranch || 'main' } ]; console.log((0, visual_1.createTable)(summaryData, { title: 'πŸ“Š Analysis Summary', keyColor: 'cyan', valueColor: 'white', separatorColor: 'gray' })); // Store analysis ID for later use config_1.default.set('last_analysis_id', savedAnalysis.analysis_id); config_1.default.set('last_repo_name', repoName); // Store the full analysis data for generate-docs (which needs the full data) const analysisDataPath = path_1.default.join(process.cwd(), '.doclyft-analysis.json'); await fs_1.promises.writeFile(analysisDataPath, JSON.stringify(analysis, null, 2)); console.log(''); console.log((0, visual_1.createSuccessMessage)('Analysis Complete!', [ 'Repository analysis saved successfully', 'Analysis data cached locally for documentation generation', 'Ready to generate documentation' ])); console.log((0, visual_1.createBox)([ 'πŸš€ Next Steps:', '', 'β€’ Generate README: doclyft generate readme', 'β€’ Generate full docs: doclyft generate docs', 'β€’ View analysis: doclyft analyze list', '', 'πŸ’‘ Tip: Use "doclyft generate --help" for more options' ], { title: '⚑ Quick Actions', style: 'single', color: 'green', padding: 1, width: 65 })); } catch (error) { console.log(''); if (error instanceof Error) { console.log((0, visual_1.createErrorMessage)('Analysis Failed', [ error.message, 'Check your repository name and GitHub token', 'Ensure you have proper repository access permissions' ])); } else { console.log((0, visual_1.createErrorMessage)('Analysis Failed', [ 'An unknown error occurred during analysis', 'Please try again or contact support' ])); } } console.log('\n'); }); analyze .command('list') .description('List previously analyzed repositories') .action(async () => { try { const userId = config_1.default.get('user_id'); const userEmail = config_1.default.get('user_email'); if (!userId || !userEmail) { console.log(chalk_1.default.red('User information not found. Please run `doclyft login` first.')); return; } const spinner = (0, ora_1.default)('Fetching analyzed repositories...').start(); // Get list of analyzed repositories 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('\nπŸ’‘ Analyze a repository using:')); console.log(chalk_1.default.cyan(' doclyft analyze repo --repo owner/name')); 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((0, visual_1.createBox)([ 'πŸ“– Generate Documentation:', '', 'β€’ Select repository: doclyft analyze select', 'β€’ Generate README: doclyft generate readme', 'β€’ Generate full docs: doclyft generate docs', '', 'πŸ’‘ Use "doclyft generate --help" for more options' ], { title: 'πŸš€ Next Steps', style: 'single', color: 'green', padding: 1, width: 65 })); } catch (error) { console.log(''); if (error instanceof Error) { console.log((0, visual_1.createErrorMessage)('Failed to Fetch Repository List', [ error.message, 'Check your internet connection', 'Verify your authentication is valid' ])); } else { console.log((0, visual_1.createErrorMessage)('Failed to Fetch Repository List', [ 'An unknown error occurred', 'Please try again or contact support' ])); } } console.log('\n'); }); analyze .command('select') .description('Select and load a previously analyzed repository') .action(async () => { try { const userId = config_1.default.get('user_id'); const userEmail = config_1.default.get('user_email'); if (!userId || !userEmail) { console.log(chalk_1.default.red('User information not found. Please run `doclyft login` first.')); return; } 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.')); console.log(chalk_1.default.blue('\nπŸ’‘ Analyze a repository using:')); console.log(chalk_1.default.cyan(' doclyft analyze repo --repo owner/name')); return; } const choices = repos.map((repo, index) => ({ 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 to work with:', choices, initial: 0 }); if (!response.selectedRepo) { console.log(chalk_1.default.yellow('No repository selected.')); return; } const selectedRepo = response.selectedRepo; // Fetch the full analysis data for this repository const spinner2 = (0, ora_1.default)('Loading repository analysis...').start(); const analysisData = await api_1.default.getAnalysisData(selectedRepo.id); // Store the analysis data locally for generate commands const analysisDataPath = path_1.default.join(process.cwd(), '.doclyft-analysis.json'); await fs_1.promises.writeFile(analysisDataPath, JSON.stringify(analysisData, null, 2)); // Update config 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 successfully!`)); console.log(chalk_1.default.blue('\nπŸ“Š Repository Summary:')); console.log(` Repository: ${selectedRepo.repo_full_name}`); console.log(` Languages: ${selectedRepo.languages?.join(', ') || 'N/A'}`); console.log(` Files: ${selectedRepo.total_files || 'N/A'}`); console.log(` Lines: ${selectedRepo.total_lines?.toLocaleString() || 'N/A'}`); console.log(chalk_1.default.green('\nβœ… You can now generate documentation using:')); console.log(chalk_1.default.cyan(' doclyft generate readme')); console.log(chalk_1.default.cyan(' doclyft generate docs')); } catch (error) { if (error instanceof Error) { console.log(chalk_1.default.red(`❌ Failed to load repository: ${error.message}`)); } else { console.log(chalk_1.default.red('❌ An unknown error occurred.')); } } }); const generate = program.command('generate') .description('Generate documentation from analyzed repository'); // Add a command to list generated READMEs generate .command('list') .description('List all generated READMEs') .action(async () => { console.log('\n'); console.log((0, visual_1.createHeader)('πŸ“ Generated Documentation Files', { style: 'banner', color: 'green', width: 75 })); try { const currentDir = process.cwd(); const readmeFiles = []; const spinner = (0, ora_1.default)('πŸ” Scanning for README files...').start(); // Look for README files in current directory and subdirectories const checkDirectory = async (dir, depth = 0) => { if (depth > 2) return; // Limit search depth 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); spinner.stop(); if (readmeFiles.length === 0) { console.log(''); console.log((0, visual_1.createStatus)('info', 'No README files found in current directory')); console.log((0, visual_1.createBox)([ 'πŸš€ Generate Your First README', '', 'Create a beautiful README for your project:', '', 'β€’ Basic README: doclyft generate readme', 'β€’ Comprehensive docs: doclyft generate docs', 'β€’ Custom output: doclyft generate readme --output MyREADME.md', '', 'πŸ’‘ Make sure to analyze your repository first!' ], { title: 'πŸ“– Quick Start', style: 'single', color: 'green', padding: 1, width: 70 })); return; } // Sort by modification date (newest first) readmeFiles.sort((a, b) => b.modified.getTime() - a.modified.getTime()); console.log(''); console.log((0, visual_1.createDivider)(`Found ${readmeFiles.length} README Files`, { color: 'green', width: 70 })); console.log(''); readmeFiles.forEach((file, index) => { const sizeKB = (file.size / 1024).toFixed(1); const modifiedTime = file.modified.toLocaleDateString() + ' ' + file.modified.toLocaleTimeString(); const fileData = [ { key: 'πŸ“„ File Path', value: file.path }, { key: 'πŸ“Š Size', value: `${sizeKB} KB` }, { key: 'πŸ“… Modified', value: modifiedTime } ]; console.log((0, visual_1.createTable)(fileData, { title: `${index + 1}. ${file.name}`, keyColor: 'cyan', valueColor: 'white', separatorColor: 'gray' })); console.log(''); }); console.log((0, visual_1.createBox)([ 'πŸš€ Deploy Your Documentation:', '', 'β€’ Push to GitHub: doclyft push --type readme --file <path>', 'β€’ Generate new README: doclyft generate readme', 'β€’ Update existing: doclyft generate readme --force', '', 'πŸ’‘ Use "doclyft push --help" for more push options' ], { title: 'πŸ“¦ Next Steps', style: 'single', color: 'blue', padding: 1, width: 65 })); } catch (error) { console.log(''); if (error instanceof Error) { console.log((0, visual_1.createErrorMessage)('Failed to List README Files', [ error.message, 'Check directory permissions', 'Ensure you have read access to the current directory' ])); } else { console.log((0, visual_1.createErrorMessage)('Failed to List README Files', [ 'An unknown error occurred', 'Please try again or contact support' ])); } } console.log('\n'); }); generate .command('readme') .description('Generate a README file') .option('--output <file>', 'Output file path', 'README.md') .option('--force', 'Overwrite existing file') .option('--deploy', 'Auto-deploy to hosted site (if hosting enabled)') .action(async (options) => { console.log('\n'); console.log((0, visual_1.createHeader)('πŸ“‹ README Generation', { style: 'banner', color: 'green', width: 70 })); // Show current working directory confirmation const currentDir = process.cwd(); const outputPath = path_1.default.resolve(currentDir, options.output); console.log(''); console.log((0, visual_1.createBox)([ 'πŸ“‹ Working Directory & Output Confirmation', '', 'DocLyft CLI will operate in your current directory:', `πŸ“ Current Dir: ${currentDir}`, `πŸ“„ README Output: ${outputPath}`, '', 'The README will be created/updated in this location', 'Make sure this is your intended project directory', '', '⚠️ Verify you\'re in the correct project root!' ], { title: 'πŸ“ Location Check', style: 'single', color: 'yellow', padding: 1, width: 75 })); // Ask for confirmation const dirConfirm = await (0, prompts_1.default)({ type: 'confirm', name: 'continue', message: 'Continue with README generation in this location?', initial: true }); if (!dirConfirm.continue) { console.log(''); console.log((0, visual_1.createStatus)('info', 'README generation cancelled by user')); console.log(''); console.log((0, visual_1.createBox)([ 'πŸ“ Change Directory Instructions:', '', '1. Use "cd" to navigate to your project directory', '2. Run the generate command again', '3. Example: cd /path/to/your/project && doclyft generate readme', '', 'πŸ’‘ Make sure you\'re in the root of your project!' ], { title: 'πŸ—ΊοΈ Navigation Help', style: 'single', color: 'blue', padding: 1, width: 65 })); return; } try { // Check if we have stored 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); } catch (error) { console.log(chalk_1.default.red('No analysis data found. Please run `doclyft analyze repo` first.')); return; } // Check if file exists and we're not forcing if (!options.force && await fs_1.promises.access(options.output).then(() => true).catch(() => false)) { const confirm = await (0, prompts_1.default)({ type: 'confirm', name: 'overwrite', message: `${options.output} already exists. Overwrite?`, initial: false }); if (!confirm.overwrite) { console.log(chalk_1.default.yellow('Cancelled.')); return; } } const userId = config_1.default.get('user_id'); const userEmail = config_1.default.get('user_email'); if (!userId || !userEmail) { console.log(''); console.log((0, visual_1.createErrorMessage)('Authentication Required', [ 'User information not found', 'Please run "doclyft login" first to authenticate' ])); return; } console.log(''); console.log((0, visual_1.createDivider)('AI Generation', { color: 'green', width: 70 })); const saveSpinner = (0, ora_1.default)('πŸ’Ύ Saving analysis to backend...').start(); // First save the analysis data to get a repository_id (for README generation) const saveResult = await api_1.default.saveAnalysis(analysisData, userId, userEmail); saveSpinner.succeed('Analysis saved to backend'); const generateSpinner = (0, ora_1.default)('πŸ€– Generating README with AI...').start(); const readmeContent = await api_1.default.generateReadme(saveResult.analysis_id); generateSpinner.text = 'πŸ“ Writing README file...'; // Ensure output directory exists await fs_1.promises.mkdir(path_1.default.dirname(options.output), { recursive: true }); await fs_1.promises.writeFile(options.output, readmeContent); generateSpinner.succeed(`Successfully generated ${options.output}`); console.log(''); console.log((0, visual_1.createDivider)('Generation Results', { color: 'green', width: 70 })); const repoName = analysisData.repository?.full_name || 'repository'; const fileStats = await fs_1.promises.stat(options.output); const fileSizeKB = (fileStats.size / 1024).toFixed(1); const resultData = [ { key: 'πŸ“ Repository', value: repoName }, { key: 'πŸ“„ Output File', value: options.output }, { key: 'πŸ“Š File Size', value: `${fileSizeKB} KB` }, { key: 'πŸ†” Analysis ID', value: saveResult.analysis_id }, { key: 'πŸ“… Generated', value: new Date().toLocaleString() } ]; console.log((0, visual_1.createTable)(resultData, { title: 'πŸŽ‰ README Generated Successfully', keyColor: 'green', valueColor: 'white', separatorColor: 'gray' })); // Import the logger const activityLogger = new activity_logger_1.ActivityLogger(); await activityLogger.init(); // Log the generation activity await activityLogger.logActivity({ type: 'generate', timestamp: new Date().toISOString(), repo: analysisData.repository?.full_name, analysis_id: saveResult.analysis_id, files: [options.output], user_id: userId }); // Sync activities to backend try { await activityLogger.syncLogsToBackend(); } catch (syncError) { console.log((0, visual_1.createStatus)('warning', 'Could not sync activities to backend')); } console.log(''); // Ask if user wants to edit the README const editChoice = await (0, prompts_1.default)({ type: 'confirm', name: 'editWithEditor', message: 'Would you like to review/edit it before saving?', initial: true }); if (editChoice.editWithEditor) { // Use user's preferred editor or fallback to nano const editor = process.env.EDITOR || 'nano'; console.log(chalk_1.default.blue(`\nπŸ“ Opening README in ${editor}...`)); if (editor === 'nano' || editor === 'vim' || editor === 'vi') { console.log(chalk_1.default.yellow('πŸ’‘ Use Ctrl+O to save, Ctrl+X to exit')); } try { const { spawn } = await Promise.resolve().then(() => __importStar(require('child_process'))); const editorProcess = spawn(editor, [options.output], { stdio: 'inherit', shell: true // This helps with Windows compatibility }); await new Promise((resolve, reject) => { editorProcess.on('close', (code) => { if (code === 0) { console.log(chalk_1.default.green('\nβœ… Changes saved. Ready to push or export.')); // Log the edit activity activityLogger.logActivity({ type: 'edit', timestamp: new Date().toISOString(), repo: analysisData.repository?.full_name, files: [options.output], path: options.output, user_id: userId }).catch(() => { }); // Ignore logging errors resolve(); } else { console.log(chalk_1.default.yellow('\n⚠️ Editor closed with code:', code)); resolve(); // Don't reject, just continue } }); editorProcess.on('error', (err) => { if (err.message.includes('ENOENT')) { console.log(chalk_1.default.red(`\n❌ Editor "${editor}" not found. Please set the EDITOR environment variable or edit the file manually.`)); resolve(); // Don't reject, just continue } else { reject(err); } }); }); } catch (error) { console.log(chalk_1.default.red(`❌ Error opening editor: ${error instanceof Error ? error.message : 'Unknown error'}`)); console.log(chalk_1.default.blue('You can still edit the file manually.')); } } // Handle auto-deploy if requested if (options.deploy && analysisData.repository?.full_name) { console.log(chalk_1.default.blue('\nπŸš€ Checking for hosted documentation...')); try { const hostingStatus = await api_1.default.getHostingStatus(analysisData.repository.full_name); if (hostingStatus.success && hostingStatus.data?.hosting_enabled) { console.log(chalk_1.default.green('βœ… Hosting enabled, triggering deployment...')); const deployResult = await api_1.default.deployHosting(analysisData.repository.full_name); if (deployResult.success && deployResult.data) { console.log(chalk_1.default.green('πŸŽ‰ Documentation deployed successfully!')); console.log(` Live URL: ${chalk_1.default.cyan(deployResult.data.site_url)}`); } else { console.log(chalk_1.default.yellow('⚠️ Deployment failed:', deployResult.error)); } } else { console.log(chalk_1.default.yellow('⚠️ Hosting not enabled for this repository')); console.log(chalk_1.default.blue('πŸ’‘ To enable hosting:')); console.log(chalk_1.default.cyan(` doclyft hosting enable --repo ${analysisData.repository.full_name}`)); } } catch (deployError) { console.log(chalk_1.default.yellow('⚠️ Auto-deploy failed:', deployError instanceof Error ? deployError.message : 'Unknown error')); } } console.log(''); console.log((0, visual_1.createSuccessMessage)('README Generation Complete!', [ 'Your AI-generated README is ready to use', 'File saved successfully to disk', 'Ready for deployment or further editing' ])); console.log((0, visual_1.createBox)([ 'πŸš€ Next Steps:', '', 'β€’ Push to GitHub: doclyft push --type readme', 'β€’ View your README: cat ' + options.output, 'β€’ Edit manually: code ' + options.output, '', 'πŸ’‘ Deploy Options:', 'β€’ Auto-deploy: doclyft generate readme --deploy', 'β€’ Enable hosting: doclyft hosting enable' ], { title: '⚑ Quick Actions', style: 'single', color: 'blue', padding: 1, width: 65 })); } catch (error) { console.log(''); if (error instanceof Error) { console.log((0, visual_1.createErrorMessage)('README Generation Failed', [ error.message, 'Check your internet connection', 'Verify your analysis data is valid', 'Try running the analysis again if needed' ])); } else {