doclyft
Version:
CLI for DocLyft - Interactive documentation generator with hosted documentation support
1,095 lines (1,094 loc) β’ 151 kB
JavaScript
#!/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 {