doclyft
Version:
CLI for DocLyft - Interactive documentation generator with hosted documentation support
1,043 lines โข 53.5 kB
JavaScript
"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