a new start

This commit is contained in:
valknarness
2025-10-25 15:52:06 +02:00
commit 700c73bcbf
28 changed files with 7295 additions and 0 deletions

98
lib/banner.js Normal file
View File

@@ -0,0 +1,98 @@
const chalk = require('chalk');
const gradient = require('gradient-string');
const figlet = require('figlet');
// Theme colors
const purpleGold = gradient(['#DA22FF', '#9733EE', '#FFD700']);
const pinkPurple = gradient(['#FF1493', '#DA22FF', '#9733EE']);
const goldPink = gradient(['#FFD700', '#FF69B4', '#FF1493']);
// Awesome ASCII logo inspired by the official logo
const awesomeLogo = `
▄████████ ▄█ █▄ ▄████████ ▄████████ ▄██████▄ ▄▄▄▄███▄▄▄▄ ▄████████
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ▄██▀▀▀███▀▀▀██▄ ███ ███
███ ███ ███ ███ ███ █▀ ███ █▀ ███ ███ ███ ███ ███ ███ █▀
███ ███ ███ ███ ▄███▄▄▄ ███ ███ ███ ███ ███ ███ ▄███▄▄▄
▀███████████ ███ ███ ▀▀███▀▀▀ ▀███████████ ███ ███ ███ ███ ███ ▀▀███▀▀▀
███ ███ ███ ███ ███ █▄ ███ ███ ███ ███ ███ ███ ███ █▄
███ ███ ███ ▄█▄ ███ ███ ███ ▄█ ███ ███ ███ ███ ███ ███ ███ ███
███ █▀ ▀███▀███▀ ██████████ ▄████████▀ ▀██████▀ ▀█ ███ █▀ ██████████
`;
// Simplified awesome logo for smaller terminals
const simpleAwesomeLogo = `
╔═╗╦ ╦╔═╗╔═╗╔═╗╔╦╗╔═╗
╠═╣║║║║╣ ╚═╗║ ║║║║║╣
╩ ╩╚╩╝╚═╝╚═╝╚═╝╩ ╩╚═╝
`;
// Display the banner
function showBanner(simple = false) {
console.clear();
if (simple) {
console.log(purpleGold(simpleAwesomeLogo));
} else {
console.log(purpleGold(awesomeLogo));
}
console.log(pinkPurple(' A curated list explorer for the curious mind\n'));
console.log(chalk.gray(' ━'.repeat(40)));
console.log();
}
// Show a figlet banner with custom text
function showFigletBanner(text, font = 'Standard') {
return new Promise((resolve, reject) => {
figlet.text(text, { font }, (err, data) => {
if (err) {
reject(err);
return;
}
console.log(purpleGold(data));
resolve();
});
});
}
// Display section header
function sectionHeader(title, icon = '') {
console.log();
console.log(purpleGold(`${icon} ${title} ${icon}`));
console.log(chalk.gray('━'.repeat(title.length + (icon ? 6 : 2))));
console.log();
}
// Display animated loading banner
async function showLoadingBanner(message = 'Loading...') {
console.log();
console.log(goldPink(`${message}`));
console.log();
}
// Display success banner
function showSuccessBanner(message) {
console.log();
console.log(chalk.green(`${message}`));
console.log();
}
// Display error banner
function showErrorBanner(message) {
console.log();
console.log(chalk.red(`${message}`));
console.log();
}
// Export functions and colors
module.exports = {
showBanner,
showFigletBanner,
sectionHeader,
showLoadingBanner,
showSuccessBanner,
showErrorBanner,
purpleGold,
pinkPurple,
goldPink
};

196
lib/bookmarks.js Normal file
View File

@@ -0,0 +1,196 @@
const inquirer = require('inquirer');
const chalk = require('chalk');
const Table = require('cli-table3');
const { purpleGold, sectionHeader } = require('./banner');
const db = require('./db-operations');
// Manage bookmarks
async function manage() {
console.clear();
sectionHeader('MY BOOKMARKS', '⭐');
const bookmarks = db.getBookmarks();
if (bookmarks.length === 0) {
console.log(chalk.yellow(' No bookmarks yet. Search and bookmark your favorite projects!\n'));
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
return;
}
console.log(chalk.hex('#FFD700')(` ${bookmarks.length} bookmarks\n`));
// Display bookmarks table
const table = new Table({
head: [
chalk.hex('#DA22FF')('#'),
chalk.hex('#DA22FF')('Name'),
chalk.hex('#DA22FF')('Tags'),
chalk.hex('#DA22FF')('⭐')
],
colWidths: [5, 30, 30, 7],
wordWrap: true,
style: {
head: [],
border: ['gray']
}
});
bookmarks.slice(0, 20).forEach((bookmark, idx) => {
table.push([
chalk.gray(idx + 1),
chalk.hex('#FF69B4')(bookmark.name),
bookmark.tags ? chalk.hex('#FFD700')(bookmark.tags) : chalk.gray('No tags'),
chalk.hex('#9733EE')(bookmark.stars || '-')
]);
});
console.log(table.toString());
console.log();
// Let user select a bookmark
const choices = bookmarks.map((bookmark, idx) => ({
name: `${idx + 1}. ${chalk.hex('#FF69B4')(bookmark.name)} ${chalk.gray('-')} ${bookmark.description || 'No description'}`,
value: bookmark
}));
choices.push(new inquirer.Separator());
choices.push({ name: chalk.gray('← Back'), value: null });
const { selected } = await inquirer.prompt([
{
type: 'list',
name: 'selected',
message: 'Select a bookmark:',
choices: choices,
pageSize: 15
}
]);
if (selected) {
await viewBookmark(selected);
}
}
// View single bookmark
async function viewBookmark(bookmark) {
console.clear();
console.log(purpleGold(`\n${bookmark.name}\n`));
console.log(chalk.gray('━'.repeat(70)));
console.log(chalk.hex('#DA22FF')(' URL: ') + chalk.cyan(bookmark.url));
console.log(chalk.hex('#FF69B4')(' Description:') + ` ${bookmark.description || chalk.gray('No description')}`));
console.log(chalk.hex('#FFD700')(' Language: ') + ` ${bookmark.language || chalk.gray('Unknown')}`);
console.log(chalk.hex('#9733EE')(' Stars: ') + ` ${bookmark.stars || '0'}`);
if (bookmark.tags) {
console.log(chalk.hex('#DA22FF')(' Tags: ') + chalk.hex('#FFD700')(bookmark.tags));
}
if (bookmark.categories) {
console.log(chalk.hex('#FF69B4')(' Categories: ') + chalk.hex('#9733EE')(bookmark.categories));
}
if (bookmark.notes) {
console.log();
console.log(chalk.hex('#FFD700')(' Notes:'));
console.log(chalk.gray(` ${bookmark.notes}`));
}
console.log(chalk.gray('━'.repeat(70)));
console.log();
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: chalk.hex('#DA22FF')('📖 Read README'), value: 'read' },
{ name: chalk.hex('#FF69B4')('✏️ Edit bookmark'), value: 'edit' },
{ name: chalk.hex('#FFD700')('🗑️ Remove bookmark'), value: 'remove' },
{ name: chalk.hex('#9733EE')('🌐 Open in browser'), value: 'browser' },
{ name: chalk.gray('← Back'), value: 'back' }
]
}
]);
switch (action) {
case 'read':
const readme = db.getReadme(bookmark.repository_id);
if (readme) {
const viewer = require('./viewer');
const repo = db.getRepository(bookmark.repository_id);
await viewer.viewReadme(repo, readme);
} else {
console.log(chalk.yellow('\n README not indexed\n'));
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter...' }]);
}
await viewBookmark(bookmark);
break;
case 'edit':
await editBookmark(bookmark);
await manage();
break;
case 'remove':
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Remove this bookmark?',
default: false
}
]);
if (confirm) {
db.removeBookmark(bookmark.repository_id);
console.log(chalk.green('\n ✓ Bookmark removed\n'));
await new Promise(resolve => setTimeout(resolve, 1000));
}
await manage();
break;
case 'browser':
const { spawn } = require('child_process');
spawn('xdg-open', [bookmark.url], { detached: true, stdio: 'ignore' });
await viewBookmark(bookmark);
break;
case 'back':
await manage();
break;
}
}
// Edit bookmark
async function editBookmark(bookmark) {
const { notes, tags, categories } = await inquirer.prompt([
{
type: 'input',
name: 'notes',
message: 'Notes:',
default: bookmark.notes || ''
},
{
type: 'input',
name: 'tags',
message: 'Tags (comma-separated):',
default: bookmark.tags || ''
},
{
type: 'input',
name: 'categories',
message: 'Categories (comma-separated):',
default: bookmark.categories || ''
}
]);
db.addBookmark(bookmark.repository_id, notes, tags, categories);
console.log(chalk.green('\n ✓ Bookmark updated!\n'));
await new Promise(resolve => setTimeout(resolve, 1000));
}
module.exports = {
manage,
viewBookmark,
editBookmark
};

212
lib/browser.js Normal file
View File

@@ -0,0 +1,212 @@
const inquirer = require('inquirer');
const chalk = require('chalk');
const { purpleGold, pinkPurple, sectionHeader } = require('./banner');
const github = require('./github-api');
const indexer = require('./indexer');
const db = require('./db-operations');
// Browse awesome lists
async function browse() {
console.clear();
sectionHeader('BROWSE AWESOME LISTS', '🌟');
const { choice } = await inquirer.prompt([
{
type: 'list',
name: 'choice',
message: 'What would you like to browse?',
choices: [
{ name: chalk.hex('#DA22FF')('🌐 Fetch from GitHub (sindresorhus/awesome)'), value: 'github' },
{ name: chalk.hex('#FF69B4')('💾 Browse indexed lists'), value: 'indexed' },
{ name: chalk.gray('← Back'), value: 'back' }
]
}
]);
if (choice === 'github') {
await browseFromGitHub();
} else if (choice === 'indexed') {
await browseIndexed();
}
}
// Browse from GitHub
async function browseFromGitHub() {
console.log(chalk.hex('#FFD700')('\n Fetching awesome lists from GitHub...\n'));
try {
const markdown = await github.getAwesomeListsIndex();
const lists = indexer.parseMarkdownLinks(markdown);
// Group by category
const byCategory = {};
lists.forEach(list => {
const cat = list.category || 'Uncategorized';
if (!byCategory[cat]) byCategory[cat] = [];
byCategory[cat].push(list);
});
// Let user select category
const categories = Object.keys(byCategory).sort();
const { category } = await inquirer.prompt([
{
type: 'list',
name: 'category',
message: 'Select a category:',
choices: categories.map(cat => ({
name: `${chalk.hex('#DA22FF')(cat)} ${chalk.gray(`(${byCategory[cat].length} lists)`)}`,
value: cat
})),
pageSize: 15
}
]);
// Let user select list
const categoryLists = byCategory[category];
const choices = categoryLists.map(list => ({
name: `${chalk.hex('#FF69B4')(list.name)} ${chalk.gray('-')} ${list.description}`,
value: list
}));
choices.push(new inquirer.Separator());
choices.push({ name: chalk.gray('← Back'), value: null });
const { selected } = await inquirer.prompt([
{
type: 'list',
name: 'selected',
message: 'Select an awesome list:',
choices: choices,
pageSize: 15
}
]);
if (selected) {
await viewList(selected);
}
} catch (error) {
console.error(chalk.red('\nError fetching lists:'), error.message);
}
}
// Browse indexed lists
async function browseIndexed() {
const lists = db.getAllAwesomeLists();
if (lists.length === 0) {
console.log(chalk.yellow('\n No lists indexed yet. Run "awesome index" first.\n'));
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
return;
}
const choices = lists.map(list => ({
name: `${chalk.hex('#FF69B4')(list.name)} ${chalk.gray('-')} ${list.description || 'No description'}`,
value: list
}));
choices.push(new inquirer.Separator());
choices.push({ name: chalk.gray('← Back'), value: null });
const { selected } = await inquirer.prompt([
{
type: 'list',
name: 'selected',
message: 'Select a list to view:',
choices: choices,
pageSize: 15
}
]);
if (selected) {
await viewIndexedList(selected);
}
}
// View awesome list details
async function viewList(list) {
console.clear();
console.log(purpleGold(`\n${list.name}\n`));
console.log(chalk.gray('━'.repeat(70)));
console.log(chalk.hex('#DA22FF')(' Category: ') + chalk.hex('#FFD700')(list.category || 'Uncategorized'));
console.log(chalk.hex('#FF69B4')(' URL: ') + chalk.cyan(list.url));
console.log(chalk.hex('#9733EE')(' Description: ') + (list.description || chalk.gray('No description')));
console.log(chalk.gray('━'.repeat(70)));
console.log();
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: chalk.hex('#DA22FF')('📥 Index this list'), value: 'index' },
{ name: chalk.hex('#FF69B4')('🌐 Open in browser'), value: 'browser' },
{ name: chalk.gray('← Back'), value: 'back' }
]
}
]);
if (action === 'index') {
await indexSingleList(list);
} else if (action === 'browser') {
const { spawn } = require('child_process');
spawn('xdg-open', [list.url], { detached: true, stdio: 'ignore' });
await viewList(list);
}
}
// View indexed list
async function viewIndexedList(list) {
const repos = db.getRepositoriesByList(list.id);
console.clear();
console.log(purpleGold(`\n${list.name}\n`));
console.log(chalk.gray('━'.repeat(70)));
console.log(chalk.hex('#FFD700')(` ${repos.length} repositories`));
console.log(chalk.gray('━'.repeat(70)));
console.log();
if (repos.length === 0) {
console.log(chalk.yellow(' No repositories indexed\n'));
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
return;
}
const choices = repos.slice(0, 50).map(repo => ({
name: `${chalk.hex('#FF69B4')(repo.name)} ${chalk.gray(`(⭐ ${repo.stars || 0})`)}`,
value: repo
}));
choices.push(new inquirer.Separator());
choices.push({ name: chalk.gray('← Back'), value: null });
const { selected } = await inquirer.prompt([
{
type: 'list',
name: 'selected',
message: 'Select a repository:',
choices: choices,
pageSize: 15
}
]);
if (selected) {
const search = require('./search');
await search.viewRepository({ repository_id: selected.id });
await viewIndexedList(list);
}
}
// Index a single list
async function indexSingleList(list) {
console.log(chalk.hex('#FFD700')('\n Indexing list...\n'));
// This would trigger the indexer for just this list
console.log(chalk.gray(' Use "awesome index" for full indexing\n'));
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
}
module.exports = {
browse,
browseFromGitHub,
browseIndexed
};

97
lib/checkout.js Normal file
View File

@@ -0,0 +1,97 @@
const simpleGit = require('simple-git');
const inquirer = require('inquirer');
const chalk = require('chalk');
const path = require('path');
const os = require('os');
const { purpleGold, goldPink } = require('./banner');
// Clone repository
async function cloneRepository(repoUrl, targetDir) {
console.clear();
console.log(goldPink('\n📥 CLONE REPOSITORY 📥\n'));
// Parse repo name from URL
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/\?#]+)/);
if (!match) {
console.log(chalk.red(' Invalid GitHub URL\n'));
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter...' }]);
return;
}
const [, owner, repo] = match;
const repoName = repo.replace(/\.git$/, '');
// Determine target directory
let directory = targetDir;
if (!directory) {
const { dir } = await inquirer.prompt([
{
type: 'input',
name: 'dir',
message: 'Clone to directory:',
default: path.join(os.homedir(), 'Projects', repoName)
}
]);
directory = dir;
}
console.log(chalk.hex('#DA22FF')(`\n Repository: ${owner}/${repoName}`));
console.log(chalk.hex('#FF69B4')(` Target: ${directory}\n`));
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Start cloning?',
default: true
}
]);
if (!confirm) return;
console.log(chalk.hex('#FFD700')('\n Cloning repository...\n'));
try {
const git = simpleGit();
await git.clone(repoUrl, directory, ['--progress']);
console.log(chalk.green(`\n ✓ Successfully cloned to ${directory}\n`));
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: chalk.hex('#DA22FF')('📂 Open in file manager'), value: 'open' },
{ name: chalk.hex('#FF69B4')('📋 Copy path to clipboard'), value: 'copy' },
{ name: chalk.gray('← Back'), value: 'back' }
]
}
]);
if (action === 'open') {
const { spawn } = require('child_process');
spawn('xdg-open', [directory], { detached: true, stdio: 'ignore' });
} else if (action === 'copy') {
try {
const { spawn } = require('child_process');
const proc = spawn('xclip', ['-selection', 'clipboard']);
proc.stdin.write(directory);
proc.stdin.end();
console.log(chalk.green('\n ✓ Path copied to clipboard!\n'));
} catch (error) {
console.log(chalk.yellow('\n Install xclip to use clipboard feature\n'));
}
}
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
console.error(chalk.red('\n ✗ Clone failed:'), error.message, '\n');
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter...' }]);
}
}
module.exports = {
cloneRepository
};

414
lib/custom-lists.js Normal file
View File

@@ -0,0 +1,414 @@
const inquirer = require('inquirer');
const chalk = require('chalk');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { purpleGold, goldPink, sectionHeader } = require('./banner');
const { getDb } = require('./database');
// Manage custom lists
async function manage() {
console.clear();
sectionHeader('MY CUSTOM LISTS', '📝');
const db = getDb();
const lists = db.prepare('SELECT * FROM custom_lists ORDER BY updated_at DESC').all();
if (lists.length === 0) {
console.log(chalk.yellow(' No custom lists yet. Create your first awesome list!\n'));
} else {
console.log(chalk.hex('#FFD700')(` ${lists.length} custom lists\n`));
lists.forEach((list, idx) => {
const items = db.prepare('SELECT COUNT(*) as count FROM custom_list_items WHERE custom_list_id = ?').get(list.id);
console.log(` ${chalk.gray((idx + 1) + '.')} ${list.icon} ${chalk.hex('#FF69B4')(list.title)} ${chalk.gray(`(${items.count} items)`)}`);
if (list.description) {
console.log(` ${chalk.gray(list.description)}`);
}
});
console.log();
}
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: chalk.hex('#DA22FF')(' Create new list'), value: 'create' },
...(lists.length > 0 ? [
{ name: chalk.hex('#FF69B4')('📋 View/Edit list'), value: 'view' },
{ name: chalk.hex('#FFD700')('💾 Export list'), value: 'export' }
] : []),
{ name: chalk.gray('← Back'), value: 'back' }
]
}
]);
switch (action) {
case 'create':
await createList();
await manage();
break;
case 'view':
const { selectedList } = await inquirer.prompt([
{
type: 'list',
name: 'selectedList',
message: 'Select a list:',
choices: lists.map(list => ({
name: `${list.icon} ${list.title}`,
value: list
}))
}
]);
await viewList(selectedList);
await manage();
break;
case 'export':
const { listToExport } = await inquirer.prompt([
{
type: 'list',
name: 'listToExport',
message: 'Select a list to export:',
choices: lists.map(list => ({
name: `${list.icon} ${list.title}`,
value: list
}))
}
]);
await exportList(listToExport);
await manage();
break;
case 'back':
break;
}
}
// Create new custom list
async function createList() {
const { title, description, author, icon, badgeStyle, badgeColor } = await inquirer.prompt([
{
type: 'input',
name: 'title',
message: 'List title:',
validate: input => input.trim() ? true : 'Title is required'
},
{
type: 'input',
name: 'description',
message: 'Description:',
default: ''
},
{
type: 'input',
name: 'author',
message: 'Author name:',
default: os.userInfo().username
},
{
type: 'input',
name: 'icon',
message: 'Icon (emoji):',
default: '📚'
},
{
type: 'list',
name: 'badgeStyle',
message: 'Badge style:',
choices: [
{ name: 'Flat', value: 'flat' },
{ name: 'Flat Square', value: 'flat-square' },
{ name: 'Plastic', value: 'plastic' },
{ name: 'For the Badge', value: 'for-the-badge' }
],
default: 'flat'
},
{
type: 'list',
name: 'badgeColor',
message: 'Badge color:',
choices: [
{ name: 'Purple', value: 'blueviolet' },
{ name: 'Pink', value: 'ff69b4' },
{ name: 'Gold', value: 'FFD700' },
{ name: 'Blue', value: 'informational' },
{ name: 'Green', value: 'success' }
],
default: 'blueviolet'
}
]);
const db = getDb();
const stmt = db.prepare(`
INSERT INTO custom_lists (title, description, author, icon, badge_style, badge_color)
VALUES (?, ?, ?, ?, ?, ?)
`);
stmt.run(title, description, author, icon, badgeStyle, badgeColor);
console.log(chalk.green('\n ✓ Custom list created!\n'));
await new Promise(resolve => setTimeout(resolve, 1000));
}
// View/edit custom list
async function viewList(list) {
const db = getDb();
const items = db.prepare(`
SELECT cli.*, r.name, r.url, r.description, r.stars
FROM custom_list_items cli
JOIN repositories r ON r.id = cli.repository_id
WHERE cli.custom_list_id = ?
ORDER BY cli.order_index, cli.added_at
`).all(list.id);
console.clear();
console.log(purpleGold(`\n${list.icon} ${list.title}\n`));
console.log(chalk.gray('━'.repeat(70)));
if (list.description) console.log(chalk.gray(list.description));
if (list.author) console.log(chalk.gray(`By ${list.author}`));
console.log(chalk.gray('━'.repeat(70)));
console.log(chalk.hex('#FFD700')(`\n ${items.length} items\n`));
if (items.length > 0) {
items.forEach((item, idx) => {
console.log(` ${chalk.gray((idx + 1) + '.')} ${chalk.hex('#FF69B4')(item.name)} ${chalk.gray(`(⭐ ${item.stars || 0})`)}`);
if (item.notes) {
console.log(` ${chalk.gray(item.notes)}`);
}
});
console.log();
}
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: chalk.hex('#DA22FF')(' Add item'), value: 'add' },
...(items.length > 0 ? [{ name: chalk.hex('#FF69B4')('🗑️ Remove item'), value: 'remove' }] : []),
{ name: chalk.hex('#FFD700')('💾 Export'), value: 'export' },
{ name: chalk.hex('#9733EE')('🗑️ Delete list'), value: 'delete' },
{ name: chalk.gray('← Back'), value: 'back' }
]
}
]);
switch (action) {
case 'add':
await addToList(list.id);
await viewList(list);
break;
case 'remove':
const { itemToRemove } = await inquirer.prompt([
{
type: 'list',
name: 'itemToRemove',
message: 'Select item to remove:',
choices: items.map(item => ({
name: item.name,
value: item
}))
}
]);
const removeStmt = db.prepare('DELETE FROM custom_list_items WHERE id = ?');
removeStmt.run(itemToRemove.id);
console.log(chalk.green('\n ✓ Item removed\n'));
await new Promise(resolve => setTimeout(resolve, 1000));
await viewList(list);
break;
case 'export':
await exportList(list);
await viewList(list);
break;
case 'delete':
const { confirmDelete } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmDelete',
message: 'Delete this list?',
default: false
}
]);
if (confirmDelete) {
const deleteStmt = db.prepare('DELETE FROM custom_lists WHERE id = ?');
deleteStmt.run(list.id);
console.log(chalk.green('\n ✓ List deleted\n'));
await new Promise(resolve => setTimeout(resolve, 1000));
}
break;
case 'back':
break;
}
}
// Add repository to custom list
async function addToList(customListId) {
const dbOps = require('./db-operations');
const bookmarks = dbOps.getBookmarks();
if (bookmarks.length === 0) {
console.log(chalk.yellow('\n No bookmarks to add. Bookmark some repositories first!\n'));
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter...' }]);
return;
}
const { selectedRepo } = await inquirer.prompt([
{
type: 'list',
name: 'selectedRepo',
message: 'Select a repository:',
choices: bookmarks.map(bookmark => ({
name: `${bookmark.name} ${chalk.gray(`(⭐ ${bookmark.stars || 0})`)}`,
value: bookmark
})),
pageSize: 15
}
]);
const { notes } = await inquirer.prompt([
{
type: 'input',
name: 'notes',
message: 'Notes (optional):',
default: ''
}
]);
const db = getDb();
const stmt = db.prepare(`
INSERT OR REPLACE INTO custom_list_items (custom_list_id, repository_id, notes)
VALUES (?, ?, ?)
`);
stmt.run(customListId, selectedRepo.repository_id, notes);
console.log(chalk.green('\n ✓ Added to list!\n'));
await new Promise(resolve => setTimeout(resolve, 1000));
}
// Export custom list
async function exportList(list) {
const { format } = await inquirer.prompt([
{
type: 'list',
name: 'format',
message: 'Export format:',
choices: [
{ name: 'Markdown (.md)', value: 'markdown' },
{ name: 'JSON (.json)', value: 'json' },
{ name: chalk.gray('PDF (not yet implemented)'), value: 'pdf', disabled: true },
{ name: chalk.gray('EPUB (not yet implemented)'), value: 'epub', disabled: true }
]
}
]);
const db = getDb();
const items = db.prepare(`
SELECT cli.*, r.name, r.url, r.description, r.stars, r.language
FROM custom_list_items cli
JOIN repositories r ON r.id = cli.repository_id
WHERE cli.custom_list_id = ?
ORDER BY cli.order_index, cli.added_at
`).all(list.id);
const defaultPath = path.join(os.homedir(), 'Downloads', `${list.title.replace(/[^a-z0-9]/gi, '-').toLowerCase()}.${format === 'markdown' ? 'md' : format}`);
const { outputPath } = await inquirer.prompt([
{
type: 'input',
name: 'outputPath',
message: 'Output path:',
default: defaultPath
}
]);
if (format === 'markdown') {
await exportMarkdown(list, items, outputPath);
} else if (format === 'json') {
await exportJSON(list, items, outputPath);
}
console.log(chalk.green(`\n ✓ Exported to: ${outputPath}\n`));
await new Promise(resolve => setTimeout(resolve, 2000));
}
// Export as Markdown
async function exportMarkdown(list, items, outputPath) {
const badgeUrl = `https://img.shields.io/badge/awesome-list-${list.badge_color}?style=${list.badge_style}`;
let content = `# ${list.icon} ${list.title}\n\n`;
content += `![Awesome](${badgeUrl})\n\n`;
if (list.description) {
content += `> ${list.description}\n\n`;
}
if (list.author) {
content += `**Author:** ${list.author}\n\n`;
}
content += `## Contents\n\n`;
items.forEach(item => {
content += `### [${item.name}](${item.url})`;
if (item.stars) {
content += ` ![Stars](https://img.shields.io/badge/⭐-${item.stars}-yellow)`;
}
if (item.language) {
content += ` ![Language](https://img.shields.io/badge/lang-${item.language}-blue)`;
}
content += `\n\n`;
if (item.description) {
content += `${item.description}\n\n`;
}
if (item.notes) {
content += `*${item.notes}*\n\n`;
}
});
content += `\n---\n\n`;
content += `*Generated with [Awesome CLI](https://github.com/yourusername/awesome) 💜*\n`;
fs.writeFileSync(outputPath, content, 'utf8');
}
// Export as JSON
async function exportJSON(list, items, outputPath) {
const data = {
title: list.title,
description: list.description,
author: list.author,
icon: list.icon,
createdAt: list.created_at,
updatedAt: list.updated_at,
items: items.map(item => ({
name: item.name,
url: item.url,
description: item.description,
stars: item.stars,
language: item.language,
notes: item.notes
}))
};
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2), 'utf8');
}
module.exports = {
manage,
createList,
viewList,
addToList,
exportList
};

239
lib/database.js Normal file
View File

@@ -0,0 +1,239 @@
const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
const os = require('os');
// Database path
const DB_DIR = path.join(os.homedir(), '.awesome');
const DB_PATH = path.join(DB_DIR, 'awesome.db');
let db = null;
// Initialize database
function initialize() {
// Ensure directory exists
if (!fs.existsSync(DB_DIR)) {
fs.mkdirSync(DB_DIR, { recursive: true });
}
// Open database
db = new Database(DB_PATH);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
// Create tables
createTables();
return db;
}
// Create database schema
function createTables() {
// Awesome lists table
db.exec(`
CREATE TABLE IF NOT EXISTS awesome_lists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url TEXT NOT NULL UNIQUE,
description TEXT,
category TEXT,
stars INTEGER DEFAULT 0,
forks INTEGER DEFAULT 0,
last_commit DATETIME,
level INTEGER DEFAULT 0,
parent_id INTEGER,
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_updated DATETIME,
FOREIGN KEY (parent_id) REFERENCES awesome_lists(id) ON DELETE CASCADE
)
`);
// Repositories/Projects table
db.exec(`
CREATE TABLE IF NOT EXISTS repositories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
awesome_list_id INTEGER NOT NULL,
name TEXT NOT NULL,
url TEXT NOT NULL UNIQUE,
description TEXT,
stars INTEGER DEFAULT 0,
forks INTEGER DEFAULT 0,
watchers INTEGER DEFAULT 0,
language TEXT,
topics TEXT,
last_commit DATETIME,
created_at DATETIME,
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (awesome_list_id) REFERENCES awesome_lists(id) ON DELETE CASCADE
)
`);
// READMEs table with content
db.exec(`
CREATE TABLE IF NOT EXISTS readmes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
repository_id INTEGER NOT NULL UNIQUE,
content TEXT,
raw_content TEXT,
version_hash TEXT,
indexed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE
)
`);
// Full-text search virtual table
db.exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS readmes_fts USING fts5(
repository_name,
description,
content,
tags,
categories,
content_rowid UNINDEXED
)
`);
// Bookmarks table
db.exec(`
CREATE TABLE IF NOT EXISTS bookmarks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
repository_id INTEGER NOT NULL UNIQUE,
notes TEXT,
tags TEXT,
categories TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE
)
`);
// Custom awesome lists (user-created collections)
db.exec(`
CREATE TABLE IF NOT EXISTS custom_lists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
author TEXT,
badge_style TEXT DEFAULT 'flat',
badge_color TEXT DEFAULT 'purple',
icon TEXT DEFAULT '📚',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Custom list items
db.exec(`
CREATE TABLE IF NOT EXISTS custom_list_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
custom_list_id INTEGER NOT NULL,
repository_id INTEGER NOT NULL,
notes TEXT,
order_index INTEGER DEFAULT 0,
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (custom_list_id) REFERENCES custom_lists(id) ON DELETE CASCADE,
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE,
UNIQUE(custom_list_id, repository_id)
)
`);
// Reading history
db.exec(`
CREATE TABLE IF NOT EXISTS reading_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
repository_id INTEGER NOT NULL,
viewed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
duration_seconds INTEGER DEFAULT 0,
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE
)
`);
// Document annotations
db.exec(`
CREATE TABLE IF NOT EXISTS annotations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
repository_id INTEGER NOT NULL,
line_number INTEGER,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE
)
`);
// Tags (extracted and user-defined)
db.exec(`
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
type TEXT DEFAULT 'user',
usage_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Categories (extracted and user-defined)
db.exec(`
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
type TEXT DEFAULT 'user',
usage_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Settings
db.exec(`
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// README update history (for diff viewing)
db.exec(`
CREATE TABLE IF NOT EXISTS readme_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
repository_id INTEGER NOT NULL,
content TEXT,
version_hash TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE
)
`);
// Create indices for better performance
db.exec(`CREATE INDEX IF NOT EXISTS idx_repos_awesome_list ON repositories(awesome_list_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_readmes_repo ON readmes(repository_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_repo ON bookmarks(repository_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_custom_list_items_list ON custom_list_items(custom_list_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_custom_list_items_repo ON custom_list_items(repository_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_reading_history_repo ON reading_history(repository_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_reading_history_time ON reading_history(viewed_at)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_annotations_repo ON annotations(repository_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_awesome_lists_parent ON awesome_lists(parent_id)`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_awesome_lists_level ON awesome_lists(level)`);
}
// Get database instance
function getDb() {
return db;
}
// Close database
function close() {
if (db) {
db.close();
}
}
// Export functions
module.exports = {
initialize,
getDb,
close,
DB_DIR,
DB_PATH
};

260
lib/db-operations.js Normal file
View File

@@ -0,0 +1,260 @@
const { getDb } = require('./database');
const crypto = require('crypto');
// Helper to get hash of content
function getContentHash(content) {
return crypto.createHash('sha256').update(content).digest('hex');
}
// Awesome Lists
function addAwesomeList(name, url, description, category, level = 0, parentId = null) {
const db = getDb();
const stmt = db.prepare(`
INSERT OR REPLACE INTO awesome_lists (name, url, description, category, level, parent_id)
VALUES (?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(name, url, description, category, level, parentId);
return result.lastInsertRowid;
}
function getAwesomeList(id) {
const db = getDb();
return db.prepare('SELECT * FROM awesome_lists WHERE id = ?').get(id);
}
function getAwesomeListByUrl(url) {
const db = getDb();
return db.prepare('SELECT * FROM awesome_lists WHERE url = ?').get(url);
}
function getAllAwesomeLists() {
const db = getDb();
return db.prepare('SELECT * FROM awesome_lists ORDER BY level, name').all();
}
function updateAwesomeListStats(id, stars, forks, lastCommit) {
const db = getDb();
const stmt = db.prepare(`
UPDATE awesome_lists
SET stars = ?, forks = ?, last_commit = ?, last_updated = CURRENT_TIMESTAMP
WHERE id = ?
`);
stmt.run(stars, forks, lastCommit, id);
}
// Repositories
function addRepository(awesomeListId, name, url, description, stats = {}) {
const db = getDb();
const stmt = db.prepare(`
INSERT OR REPLACE INTO repositories
(awesome_list_id, name, url, description, stars, forks, watchers, language, topics, last_commit, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
awesomeListId,
name,
url,
description || '',
stats.stars || 0,
stats.forks || 0,
stats.watchers || 0,
stats.language || '',
Array.isArray(stats.topics) ? stats.topics.join(',') : (stats.topics || ''),
stats.pushedAt || null,
stats.createdAt || null
);
return result.lastInsertRowid;
}
function getRepository(id) {
const db = getDb();
return db.prepare('SELECT * FROM repositories WHERE id = ?').get(id);
}
function getRepositoryByUrl(url) {
const db = getDb();
return db.prepare('SELECT * FROM repositories WHERE url = ?').get(url);
}
function getRepositoriesByList(awesomeListId) {
const db = getDb();
return db.prepare('SELECT * FROM repositories WHERE awesome_list_id = ? ORDER BY stars DESC').all(awesomeListId);
}
// READMEs
function addReadme(repositoryId, content, rawContent) {
const db = getDb();
const hash = getContentHash(rawContent);
const stmt = db.prepare(`
INSERT OR REPLACE INTO readmes (repository_id, content, raw_content, version_hash)
VALUES (?, ?, ?, ?)
`);
const result = stmt.run(repositoryId, content, rawContent, hash);
// Add to FTS index
const repo = getRepository(repositoryId);
if (repo) {
const ftsStmt = db.prepare(`
INSERT OR REPLACE INTO readmes_fts (rowid, repository_name, description, content, tags, categories)
VALUES (?, ?, ?, ?, ?, ?)
`);
ftsStmt.run(result.lastInsertRowid, repo.name, repo.description, content, repo.topics || '', '');
}
return result.lastInsertRowid;
}
function getReadme(repositoryId) {
const db = getDb();
return db.prepare('SELECT * FROM readmes WHERE repository_id = ?').get(repositoryId);
}
// Search
function searchReadmes(query, limit = 50) {
const db = getDb();
const stmt = db.prepare(`
SELECT
r.id,
r.repository_id,
r.content,
repo.name,
repo.url,
repo.description,
repo.stars,
repo.language,
repo.topics,
rank
FROM readmes_fts
JOIN readmes r ON r.id = readmes_fts.rowid
JOIN repositories repo ON repo.id = r.repository_id
WHERE readmes_fts MATCH ?
ORDER BY rank
LIMIT ?
`);
return stmt.all(query, limit);
}
// Bookmarks
function addBookmark(repositoryId, notes = '', tags = '', categories = '') {
const db = getDb();
const stmt = db.prepare(`
INSERT OR REPLACE INTO bookmarks (repository_id, notes, tags, categories, updated_at)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
`);
return stmt.run(repositoryId, notes, tags, categories);
}
function removeBookmark(repositoryId) {
const db = getDb();
return db.prepare('DELETE FROM bookmarks WHERE repository_id = ?').run(repositoryId);
}
function getBookmarks() {
const db = getDb();
return db.prepare(`
SELECT b.*, r.name, r.url, r.description, r.stars, r.language
FROM bookmarks b
JOIN repositories r ON r.id = b.repository_id
ORDER BY b.created_at DESC
`).all();
}
function isBookmarked(repositoryId) {
const db = getDb();
const result = db.prepare('SELECT COUNT(*) as count FROM bookmarks WHERE repository_id = ?').get(repositoryId);
return result.count > 0;
}
// Reading History
function addToHistory(repositoryId, durationSeconds = 0) {
const db = getDb();
const stmt = db.prepare(`
INSERT INTO reading_history (repository_id, duration_seconds)
VALUES (?, ?)
`);
return stmt.run(repositoryId, durationSeconds);
}
function getHistory(limit = 50) {
const db = getDb();
return db.prepare(`
SELECT h.*, r.name, r.url, r.description
FROM reading_history h
JOIN repositories r ON r.id = h.repository_id
ORDER BY h.viewed_at DESC
LIMIT ?
`).all(limit);
}
// Settings
function getSetting(key, defaultValue = null) {
const db = getDb();
const result = db.prepare('SELECT value FROM settings WHERE key = ?').get(key);
return result ? result.value : defaultValue;
}
function setSetting(key, value) {
const db = getDb();
const stmt = db.prepare(`
INSERT OR REPLACE INTO settings (key, value, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
`);
stmt.run(key, value);
}
// Statistics
function getStats() {
const db = getDb();
return {
awesomeLists: db.prepare('SELECT COUNT(*) as count FROM awesome_lists').get().count,
repositories: db.prepare('SELECT COUNT(*) as count FROM repositories').get().count,
readmes: db.prepare('SELECT COUNT(*) as count FROM readmes').get().count,
bookmarks: db.prepare('SELECT COUNT(*) as count FROM bookmarks').get().count,
customLists: db.prepare('SELECT COUNT(*) as count FROM custom_lists').get().count,
historyItems: db.prepare('SELECT COUNT(*) as count FROM reading_history').get().count,
annotations: db.prepare('SELECT COUNT(*) as count FROM annotations').get().count,
tags: db.prepare('SELECT COUNT(*) as count FROM tags').get().count,
categories: db.prepare('SELECT COUNT(*) as count FROM categories').get().count
};
}
// Random
function getRandomRepository() {
const db = getDb();
return db.prepare(`
SELECT * FROM repositories
WHERE id IN (SELECT repository_id FROM readmes)
ORDER BY RANDOM()
LIMIT 1
`).get();
}
module.exports = {
addAwesomeList,
getAwesomeList,
getAwesomeListByUrl,
getAllAwesomeLists,
updateAwesomeListStats,
addRepository,
getRepository,
getRepositoryByUrl,
getRepositoriesByList,
addReadme,
getReadme,
searchReadmes,
addBookmark,
removeBookmark,
getBookmarks,
isBookmarked,
addToHistory,
getHistory,
getSetting,
setSetting,
getStats,
getRandomRepository
};

218
lib/github-api.js Normal file
View File

@@ -0,0 +1,218 @@
const axios = require('axios');
const chalk = require('chalk');
const inquirer = require('inquirer');
const db = require('./db-operations');
// Rate limiting
const RATE_LIMIT_DELAY = 100;
let lastRequestTime = 0;
let rateLimitWarningShown = false;
// Get GitHub token from settings
function getGitHubToken() {
return db.getSetting('githubToken', null);
}
// Rate-limited request with better handling
async function rateLimitedRequest(url, options = {}) {
const now = Date.now();
const timeSinceLastRequest = now - lastRequestTime;
if (timeSinceLastRequest < RATE_LIMIT_DELAY) {
await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY - timeSinceLastRequest));
}
lastRequestTime = Date.now();
const token = getGitHubToken();
const headers = {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'awesome-cli',
...options.headers
};
// Add auth token if available
if (token) {
headers['Authorization'] = `token ${token}`;
}
try {
return await axios.get(url, {
timeout: 10000,
headers,
...options
});
} catch (error) {
if (error.response?.status === 403) {
const remaining = error.response.headers['x-ratelimit-remaining'];
const resetTime = parseInt(error.response.headers['x-ratelimit-reset']) * 1000;
const waitTime = Math.max(0, resetTime - Date.now());
const waitMinutes = Math.ceil(waitTime / 60000);
if (remaining === '0' || remaining === 0) {
console.log();
console.log(chalk.red('⚠️ GitHub API Rate Limit Exceeded!'));
console.log(chalk.yellow(` Wait time: ${waitMinutes} minutes`));
if (!token && !rateLimitWarningShown) {
console.log();
console.log(chalk.hex('#FFD700')('💡 TIP: Add a GitHub Personal Access Token to increase limit from 60/hour to 5000/hour!'));
console.log(chalk.gray(' 1. Go to: https://github.com/settings/tokens'));
console.log(chalk.gray(' 2. Generate new token (classic) with "public_repo" scope'));
console.log(chalk.gray(' 3. Run: awesome settings → Add GitHub token'));
rateLimitWarningShown = true;
}
console.log();
// Ask user what to do
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: `Wait ${waitMinutes} minutes and continue`, value: 'wait' },
{ name: 'Skip remaining items and continue with what we have', value: 'skip' },
{ name: 'Abort indexing', value: 'abort' }
]
}
]);
if (action === 'abort') {
throw new Error('Indexing aborted by user');
} else if (action === 'skip') {
throw new Error('SKIP_RATE_LIMIT'); // Special error to skip
} else {
// Wait with countdown
console.log(chalk.gray(`\nWaiting ${waitMinutes} minutes...`));
await new Promise(resolve => setTimeout(resolve, waitTime + 1000));
return rateLimitedRequest(url, options);
}
}
}
throw error;
}
}
// Extract owner and repo from GitHub URL
function parseGitHubUrl(url) {
const match = url.match(/github\.com\/([^\/]+)\/([^\/\?#]+)/);
if (!match) return null;
return { owner: match[1], repo: match[2].replace(/\.git$/, '') };
}
// Get repository information
async function getRepoInfo(repoUrl) {
const parsed = parseGitHubUrl(repoUrl);
if (!parsed) return null;
try {
const response = await rateLimitedRequest(
`https://api.github.com/repos/${parsed.owner}/${parsed.repo}`
);
return {
name: response.data.name,
fullName: response.data.full_name,
description: response.data.description,
stars: response.data.stargazers_count,
forks: response.data.forks_count,
watchers: response.data.watchers_count,
language: response.data.language,
topics: response.data.topics || [],
createdAt: response.data.created_at,
updatedAt: response.data.updated_at,
pushedAt: response.data.pushed_at,
homepage: response.data.homepage,
license: response.data.license?.name
};
} catch (error) {
// Silently skip 404s (deleted/moved repos) - don't clutter output
if (error.response?.status === 404) {
return null;
}
// Only log non-404 errors
console.error(chalk.red(`Failed to fetch ${parsed.owner}/${parsed.repo}:`), error.message);
return null;
}
}
// Get README content
async function getReadme(repoUrl) {
const parsed = parseGitHubUrl(repoUrl);
if (!parsed) return null;
// Try different README URLs
const urls = [
`https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/main/README.md`,
`https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/master/README.md`,
`https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/main/readme.md`,
`https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/master/readme.md`,
`https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/main/Readme.md`,
`https://raw.githubusercontent.com/${parsed.owner}/${parsed.repo}/master/Readme.md`
];
for (const url of urls) {
try {
const response = await rateLimitedRequest(url);
if (response.data) {
return {
content: response.data,
url: url
};
}
} catch (error) {
continue;
}
}
return null;
}
// Get commits information
async function getLatestCommit(repoUrl) {
const parsed = parseGitHubUrl(repoUrl);
if (!parsed) return null;
try {
const response = await rateLimitedRequest(
`https://api.github.com/repos/${parsed.owner}/${parsed.repo}/commits?per_page=1`
);
if (response.data && response.data.length > 0) {
const commit = response.data[0];
return {
sha: commit.sha,
message: commit.commit.message,
author: commit.commit.author.name,
date: commit.commit.author.date
};
}
} catch (error) {
return null;
}
return null;
}
// Get list of awesome lists from main awesome repo
async function getAwesomeListsIndex() {
try {
const response = await rateLimitedRequest(
'https://raw.githubusercontent.com/sindresorhus/awesome/main/readme.md'
);
return response.data;
} catch (error) {
throw new Error('Failed to fetch awesome lists index: ' + error.message);
}
}
module.exports = {
getRepoInfo,
getReadme,
getLatestCommit,
getAwesomeListsIndex,
parseGitHubUrl,
rateLimitedRequest
};

261
lib/github-oauth.js Normal file
View File

@@ -0,0 +1,261 @@
const axios = require('axios');
const chalk = require('chalk');
const inquirer = require('inquirer');
const { spawn } = require('child_process');
const { purpleGold, goldPink } = require('./banner');
const db = require('./db-operations');
// GitHub OAuth App credentials (you'll need to register the app)
// For now, using public client credentials pattern
const CLIENT_ID = 'Iv1.b507a08c87ecfe98'; // Example - you should register your own app
// Initiate OAuth device flow
async function authenticateWithGitHub() {
console.clear();
console.log(purpleGold('\n🔐 GITHUB AUTHENTICATION 🔐\n'));
console.log(chalk.gray('Using GitHub OAuth Device Flow for secure authentication\n'));
try {
// Step 1: Request device and user codes
console.log(chalk.hex('#FFD700')(' Step 1: Requesting authorization codes...\n'));
const deviceResponse = await axios.post(
'https://github.com/login/device/code',
{
client_id: CLIENT_ID,
scope: 'public_repo'
},
{
headers: {
'Accept': 'application/json'
}
}
);
const {
device_code,
user_code,
verification_uri,
expires_in,
interval
} = deviceResponse.data;
// Step 2: Display user code and open browser
console.log(chalk.hex('#DA22FF')(' Step 2: Complete authorization in your browser\n'));
console.log(chalk.gray('━'.repeat(70)));
console.log();
console.log(chalk.hex('#FF69B4')(' Visit: ') + chalk.cyan(verification_uri));
console.log(chalk.hex('#FFD700')(' Enter code: ') + chalk.bold.hex('#FFD700')(user_code));
console.log();
console.log(chalk.gray('━'.repeat(70)));
console.log();
const { openBrowser } = await inquirer.prompt([
{
type: 'confirm',
name: 'openBrowser',
message: 'Open browser automatically?',
default: true
}
]);
if (openBrowser) {
spawn('xdg-open', [verification_uri], { detached: true, stdio: 'ignore' });
console.log(chalk.green('\n ✓ Browser opened!\n'));
}
console.log(chalk.hex('#9733EE')(' Waiting for authorization...'));
console.log(chalk.gray(` (Code expires in ${Math.floor(expires_in / 60)} minutes)\n`));
// Step 3: Poll for access token
const token = await pollForAccessToken(device_code, interval, expires_in);
if (token) {
// Save token
db.setSetting('githubToken', token);
db.setSetting('githubAuthMethod', 'oauth');
console.log();
console.log(goldPink(' ✨ Successfully authenticated! ✨\n'));
console.log(chalk.green(' ✓ Your rate limit is now 5,000 requests/hour!\n'));
await new Promise(resolve => setTimeout(resolve, 2000));
return true;
}
return false;
} catch (error) {
console.error(chalk.red('\n ✗ Authentication failed:'), error.message);
console.log();
// Offer fallback to manual token
const { fallback } = await inquirer.prompt([
{
type: 'confirm',
name: 'fallback',
message: 'Use manual token instead?',
default: true
}
]);
if (fallback) {
return await manualTokenInput();
}
return false;
}
}
// Poll GitHub for access token
async function pollForAccessToken(deviceCode, interval, expiresIn) {
const startTime = Date.now();
const pollInterval = interval * 1000;
while (Date.now() - startTime < expiresIn * 1000) {
await new Promise(resolve => setTimeout(resolve, pollInterval));
try {
const response = await axios.post(
'https://github.com/login/oauth/access_token',
{
client_id: CLIENT_ID,
device_code: deviceCode,
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
},
{
headers: {
'Accept': 'application/json'
}
}
);
const { access_token, error } = response.data;
if (access_token) {
return access_token;
}
if (error === 'authorization_pending') {
// Still waiting for user
process.stdout.write(chalk.gray('.'));
continue;
}
if (error === 'slow_down') {
// GitHub asked us to slow down
await new Promise(resolve => setTimeout(resolve, 5000));
continue;
}
if (error === 'expired_token') {
console.log(chalk.red('\n ✗ Code expired. Please try again.'));
return null;
}
if (error === 'access_denied') {
console.log(chalk.yellow('\n ⚠️ Authorization denied by user.'));
return null;
}
} catch (error) {
// Continue polling on network errors
continue;
}
}
console.log(chalk.red('\n ✗ Timeout waiting for authorization.'));
return null;
}
// Fallback: Manual token input
async function manualTokenInput() {
console.log();
console.log(chalk.hex('#FFD700')('📝 Manual Token Setup\n'));
console.log(chalk.gray(' 1. Go to: https://github.com/settings/tokens'));
console.log(chalk.gray(' 2. Generate new token (classic)'));
console.log(chalk.gray(' 3. Select scope: public_repo'));
console.log(chalk.gray(' 4. Copy and paste the token below\n'));
const { token, openUrl } = await inquirer.prompt([
{
type: 'confirm',
name: 'openUrl',
message: 'Open GitHub tokens page?',
default: true
},
{
type: 'password',
name: 'token',
message: 'Paste your token:',
mask: '*',
validate: input => {
if (!input.trim()) {
return 'Token is required';
}
if (!input.startsWith('ghp_') && !input.startsWith('github_pat_')) {
return 'Invalid token format';
}
return true;
}
}
]);
if (openUrl) {
spawn('xdg-open', ['https://github.com/settings/tokens'], { detached: true, stdio: 'ignore' });
}
if (token && token.trim()) {
db.setSetting('githubToken', token.trim());
db.setSetting('githubAuthMethod', 'manual');
console.log(chalk.green('\n ✓ Token saved!\n'));
return true;
}
return false;
}
// Check authentication status
function isAuthenticated() {
const token = db.getSetting('githubToken', null);
return token && token !== 'null';
}
// Get authentication method
function getAuthMethod() {
return db.getSetting('githubAuthMethod', 'none');
}
// Revoke/logout
async function logout() {
const method = getAuthMethod();
console.log();
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: `Remove ${method === 'oauth' ? 'OAuth' : 'manually added'} token?`,
default: false
}
]);
if (confirm) {
db.setSetting('githubToken', 'null');
db.setSetting('githubAuthMethod', 'none');
console.log(chalk.green('\n ✓ Token removed. Rate limit back to 60/hour.\n'));
if (method === 'oauth') {
console.log(chalk.gray(' Note: Token is revoked locally. For complete security,'));
console.log(chalk.gray(' revoke at: https://github.com/settings/applications\n'));
}
}
}
module.exports = {
authenticateWithGitHub,
manualTokenInput,
isAuthenticated,
getAuthMethod,
logout
};

118
lib/history.js Normal file
View File

@@ -0,0 +1,118 @@
const inquirer = require('inquirer');
const chalk = require('chalk');
const Table = require('cli-table3');
const { purpleGold, sectionHeader } = require('./banner');
const db = require('./db-operations');
// Show reading history
async function show() {
console.clear();
sectionHeader('READING HISTORY', '📖');
const history = db.getHistory(100);
if (history.length === 0) {
console.log(chalk.yellow(' No reading history yet.\n'));
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
return;
}
console.log(chalk.hex('#FFD700')(` ${history.length} items\n`));
// Display history table
const table = new Table({
head: [
chalk.hex('#DA22FF')('#'),
chalk.hex('#DA22FF')('Name'),
chalk.hex('#DA22FF')('Viewed At'),
chalk.hex('#DA22FF')('Duration')
],
colWidths: [5, 35, 20, 12],
wordWrap: true,
style: {
head: [],
border: ['gray']
}
});
history.slice(0, 25).forEach((item, idx) => {
const date = new Date(item.viewed_at);
const duration = item.duration_seconds > 0 ? `${item.duration_seconds}s` : '-';
table.push([
chalk.gray(idx + 1),
chalk.hex('#FF69B4')(item.name),
chalk.gray(date.toLocaleString()),
chalk.hex('#FFD700')(duration)
]);
});
console.log(table.toString());
console.log();
if (history.length > 25) {
console.log(chalk.gray(` ... and ${history.length - 25} more items\n`));
}
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'Options:',
choices: [
{ name: chalk.hex('#DA22FF')('🔍 View details'), value: 'view' },
{ name: chalk.hex('#FF69B4')('🗑️ Clear history'), value: 'clear' },
{ name: chalk.gray('← Back'), value: 'back' }
]
}
]);
if (action === 'view') {
const choices = history.slice(0, 50).map((item, idx) => ({
name: `${idx + 1}. ${chalk.hex('#FF69B4')(item.name)}`,
value: item
}));
choices.push(new inquirer.Separator());
choices.push({ name: chalk.gray('← Back'), value: null });
const { selected } = await inquirer.prompt([
{
type: 'list',
name: 'selected',
message: 'Select an item:',
choices: choices,
pageSize: 15
}
]);
if (selected) {
const search = require('./search');
await search.viewRepository({ repository_id: selected.repository_id });
}
await show();
} else if (action === 'clear') {
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Clear all reading history?',
default: false
}
]);
if (confirm) {
const dbInstance = require('./database').getDb();
dbInstance.exec('DELETE FROM reading_history');
console.log(chalk.green('\n ✓ History cleared\n'));
await new Promise(resolve => setTimeout(resolve, 1000));
}
await show();
}
}
module.exports = {
show
};

284
lib/indexer.js Normal file
View File

@@ -0,0 +1,284 @@
const ora = require('ora');
const chalk = require('chalk');
const inquirer = require('inquirer');
const { nanospinner } = require('nanospinner');
const cliProgress = require('cli-progress');
const { purpleGold, pinkPurple, goldPink, sectionHeader } = require('./banner');
const github = require('./github-api');
const db = require('./db-operations');
// Parse markdown to extract links
function parseMarkdownLinks(markdown) {
const lines = markdown.split('\n');
const links = [];
let currentCategory = null;
for (const line of lines) {
// Category headers (## Category Name)
const categoryMatch = line.match(/^##\s+(.+)$/);
if (categoryMatch) {
currentCategory = categoryMatch[1].trim();
continue;
}
// List items: - [Name](url) - Description
const linkMatch = line.match(/^-\s+\[([^\]]+)\]\(([^)]+)\)(?:\s+-\s+(.+))?/);
if (linkMatch) {
const [, name, url, description] = linkMatch;
// Only GitHub URLs
if (url.includes('github.com')) {
links.push({
name: name.trim(),
url: url.trim(),
description: description ? description.trim() : '',
category: currentCategory
});
}
}
}
return links;
}
// Extract text content from markdown
function extractTextContent(markdown) {
let text = markdown;
// Remove code blocks
text = text.replace(/```[\s\S]*?```/g, '');
text = text.replace(/`[^`]+`/g, '');
// Remove images
text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1');
// Remove links but keep text
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
// Remove HTML tags
text = text.replace(/<[^>]+>/g, '');
// Remove markdown headers
text = text.replace(/^#{1,6}\s+/gm, '');
// Remove horizontal rules
text = text.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '');
// Remove list markers
text = text.replace(/^[\s]*[-*+]\s+/gm, '');
text = text.replace(/^[\s]*\d+\.\s+/gm, '');
// Normalize whitespace
text = text.replace(/\s+/g, ' ').trim();
return text;
}
// Check if URL is an awesome list (not a regular project)
function isAwesomeList(url, name, description) {
const lowerName = name.toLowerCase();
const lowerDesc = (description || '').toLowerCase();
const urlLower = url.toLowerCase();
return (
lowerName.includes('awesome') ||
lowerDesc.includes('curated list') ||
lowerDesc.includes('awesome list') ||
urlLower.includes('/awesome-')
);
}
// Build the complete index
async function buildIndex(force = false) {
console.clear();
console.log(purpleGold('\n🚀 AWESOME INDEX BUILDER 🚀\n'));
if (force) {
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: chalk.yellow('⚠️ Force rebuild will clear all indexed data (bookmarks will be preserved). Continue?'),
default: false
}
]);
if (!confirm) return;
// Clear index data (keep bookmarks)
console.log(chalk.gray('\nClearing existing index...'));
const dbInstance = require('./database').getDb();
dbInstance.exec('DELETE FROM readmes');
dbInstance.exec('DELETE FROM repositories');
dbInstance.exec('DELETE FROM awesome_lists');
console.log(chalk.green('✓ Index cleared\n'));
}
// Fetch main awesome list
const spinner = ora(chalk.hex('#DA22FF')('Fetching the awesome list of awesome lists...')).start();
let mainReadme;
try {
mainReadme = await github.getAwesomeListsIndex();
spinner.succeed(chalk.green('✓ Fetched main awesome index!'));
} catch (error) {
spinner.fail(chalk.red('✗ Failed to fetch main index'));
console.error(chalk.red(error.message));
return;
}
// Parse links from main index
console.log(chalk.hex('#FF69B4')('\n📝 Parsing awesome lists...'));
const awesomeLists = parseMarkdownLinks(mainReadme);
console.log(chalk.green(`✓ Found ${awesomeLists.length} awesome lists!\n`));
// Ask user what to index
const { indexChoice } = await inquirer.prompt([
{
type: 'list',
name: 'indexChoice',
message: 'What would you like to index?',
choices: [
{ name: '🎯 Index everything (recommended for first run)', value: 'full' },
{ name: '📋 Index lists only (metadata, no READMEs)', value: 'lists' },
{ name: '🎲 Index a random sample (10 lists)', value: 'sample' },
{ name: '🔍 Select specific categories', value: 'select' },
{ name: '← Back', value: 'cancel' }
]
}
]);
if (indexChoice === 'cancel') return;
let listsToIndex = awesomeLists;
if (indexChoice === 'sample') {
listsToIndex = awesomeLists.sort(() => 0.5 - Math.random()).slice(0, 10);
} else if (indexChoice === 'select') {
const categories = [...new Set(awesomeLists.map(l => l.category).filter(Boolean))];
const { selectedCategories } = await inquirer.prompt([
{
type: 'checkbox',
name: 'selectedCategories',
message: 'Select categories to index:',
choices: categories,
pageSize: 15
}
]);
if (selectedCategories.length === 0) {
console.log(chalk.yellow('No categories selected'));
return;
}
listsToIndex = awesomeLists.filter(l => selectedCategories.includes(l.category));
}
console.log(pinkPurple(`\n✨ Starting index of ${listsToIndex.length} awesome lists ✨\n`));
// Progress bars
const multibar = new cliProgress.MultiBar({
clearOnComplete: false,
hideCursor: true,
format: ' {bar} | {percentage}% | {value}/{total} | {name}'
}, cliProgress.Presets.shades_classic);
const listBar = multibar.create(listsToIndex.length, 0, { name: 'Lists' });
const repoBar = multibar.create(100, 0, { name: 'Repos' });
let totalRepos = 0;
let indexedRepos = 0;
let indexedReadmes = 0;
let skipped404s = 0;
// Index each awesome list
for (let i = 0; i < listsToIndex.length; i++) {
const list = listsToIndex[i];
listBar.update(i + 1, { name: `Lists: ${list.name.substring(0, 30)}` });
try {
// Add list to database
const listId = db.addAwesomeList(list.name, list.url, list.description, list.category, 1, null);
// Fetch list README
const readme = await github.getReadme(list.url);
if (!readme) continue;
// Parse repositories from the list
const repos = parseMarkdownLinks(readme.content);
totalRepos += repos.length;
repoBar.setTotal(totalRepos);
// Index repositories
for (const repo of repos) {
try {
// Get repo info from GitHub
const repoInfo = await github.getRepoInfo(repo.url);
if (repoInfo) {
const repoId = db.addRepository(listId, repoInfo.name, repo.url, repo.description || repoInfo.description, repoInfo);
indexedRepos++;
// Index README if in full mode
if (indexChoice === 'full' || indexChoice === 'sample') {
const repoReadme = await github.getReadme(repo.url);
if (repoReadme) {
const textContent = extractTextContent(repoReadme.content);
db.addReadme(repoId, textContent, repoReadme.content);
indexedReadmes++;
}
}
} else {
// Repo returned null (likely 404 - deleted/moved)
skipped404s++;
}
repoBar.update(indexedRepos, { name: `Repos: ${repo.name.substring(0, 30)}` });
} catch (error) {
// Handle rate limit skip
if (error.message === 'SKIP_RATE_LIMIT') {
console.log(chalk.yellow('\n⚠ Skipping remaining items due to rate limit...'));
break; // Exit repo loop
}
// Skip failed repos
continue;
}
}
} catch (error) {
// Skip failed lists
continue;
}
}
multibar.stop();
// Summary
console.log(goldPink('\n\n✨ INDEX BUILD COMPLETE! ✨\n'));
console.log(chalk.hex('#DA22FF')('📊 Summary:'));
console.log(chalk.gray('━'.repeat(50)));
console.log(chalk.hex('#FF69B4')(` Awesome Lists: ${chalk.bold(listsToIndex.length)}`));
console.log(chalk.hex('#FFD700')(` Repositories: ${chalk.bold(indexedRepos)}`));
console.log(chalk.hex('#DA22FF')(` READMEs: ${chalk.bold(indexedReadmes)}`));
if (skipped404s > 0) {
console.log(chalk.hex('#9733EE')(` Skipped (404): ${chalk.bold(skipped404s)} ${chalk.gray('(deleted/moved repos)')}`));
}
console.log(chalk.gray('━'.repeat(50)));
console.log();
const stats = db.getStats();
console.log(chalk.hex('#FF69B4')('🗄️ Total in Database:'));
console.log(chalk.gray(` Lists: ${stats.awesomeLists} | Repos: ${stats.repositories} | READMEs: ${stats.readmes}`));
console.log();
console.log(chalk.green('✓ You can now search and explore! Try:\n'));
console.log(chalk.gray(' • awesome search "your query"'));
console.log(chalk.gray(' • awesome shell'));
console.log(chalk.gray(' • awesome browse\n'));
}
module.exports = {
buildIndex,
parseMarkdownLinks,
extractTextContent,
isAwesomeList
};

109
lib/menu.js Normal file
View File

@@ -0,0 +1,109 @@
const inquirer = require('inquirer');
const chalk = require('chalk');
const { purpleGold, pinkPurple, sectionHeader } = require('./banner');
// Main menu
async function showMainMenu() {
while (true) {
const { choice } = await inquirer.prompt([
{
type: 'list',
name: 'choice',
message: purpleGold('What would you like to do?'),
pageSize: 12,
choices: [
{ name: `${chalk.hex('#DA22FF')('🌟')} Browse Awesome Lists`, value: 'browse' },
{ name: `${chalk.hex('#FF69B4')('🔍')} Search READMEs`, value: 'search' },
{ name: `${chalk.hex('#FFD700')('📚')} Interactive Shell`, value: 'shell' },
{ name: `${chalk.hex('#DA22FF')('🎲')} Random README`, value: 'random' },
new inquirer.Separator(chalk.gray('─'.repeat(50))),
{ name: `${chalk.hex('#FF69B4')('⭐')} My Bookmarks`, value: 'bookmarks' },
{ name: `${chalk.hex('#FFD700')('📝')} My Custom Lists`, value: 'lists' },
{ name: `${chalk.hex('#DA22FF')('📖')} Reading History`, value: 'history' },
new inquirer.Separator(chalk.gray('─'.repeat(50))),
{ name: `${chalk.hex('#FF69B4')('🔧')} Build/Rebuild Index`, value: 'index' },
{ name: `${chalk.hex('#FFD700')('📊')} Statistics`, value: 'stats' },
{ name: `${chalk.hex('#DA22FF')('⚙️')} Settings`, value: 'settings' },
new inquirer.Separator(chalk.gray('─'.repeat(50))),
{ name: chalk.gray('Exit'), value: 'exit' }
]
}
]);
if (choice === 'exit') {
console.log(pinkPurple('\n✨ Thanks for using Awesome! See you soon! ✨\n'));
process.exit(0);
}
try {
await handleMenuChoice(choice);
} catch (error) {
console.error(chalk.red('\nError:'), error.message);
console.log(chalk.gray('\nPress Enter to continue...'));
await inquirer.prompt([{ type: 'input', name: 'continue', message: '' }]);
}
}
}
// Handle menu choice
async function handleMenuChoice(choice) {
switch (choice) {
case 'browse':
const browser = require('./browser');
await browser.browse();
break;
case 'search':
const search = require('./search');
await search.interactiveSearch();
break;
case 'shell':
const shell = require('./shell');
await shell.start();
break;
case 'random':
const random = require('./random');
await random.showRandom();
break;
case 'bookmarks':
const bookmarks = require('./bookmarks');
await bookmarks.manage();
break;
case 'lists':
const customLists = require('./custom-lists');
await customLists.manage();
break;
case 'history':
const history = require('./history');
await history.show();
break;
case 'index':
const indexer = require('./indexer');
await indexer.buildIndex();
break;
case 'stats':
const stats = require('./stats');
await stats.show();
break;
case 'settings':
const settings = require('./settings');
await settings.manage();
break;
default:
console.log(chalk.yellow('Invalid choice'));
}
}
module.exports = {
showMainMenu,
handleMenuChoice
};

101
lib/random.js Normal file
View File

@@ -0,0 +1,101 @@
const chalk = require('chalk');
const inquirer = require('inquirer');
const { purpleGold, goldPink } = require('./banner');
const db = require('./db-operations');
// Show random README
async function showRandom() {
console.clear();
console.log(goldPink('\n🎲 RANDOM README DISCOVERY 🎲\n'));
const repo = db.getRandomRepository();
if (!repo) {
console.log(chalk.yellow(' No repositories indexed yet. Run "awesome index" first.\n'));
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
return;
}
const readme = db.getReadme(repo.id);
console.log(purpleGold(`${repo.name}\n`));
console.log(chalk.gray('━'.repeat(70)));
console.log(chalk.hex('#DA22FF')(' URL: ') + chalk.cyan(repo.url));
console.log(chalk.hex('#FF69B4')(' Description:') + ` ${repo.description || chalk.gray('No description')}`);
console.log(chalk.hex('#FFD700')(' Language: ') + ` ${repo.language || chalk.gray('Unknown')}`);
console.log(chalk.hex('#9733EE')(' Stars: ') + ` ${repo.stars || '0'}`);
console.log(chalk.gray('━'.repeat(70)));
console.log();
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: chalk.hex('#DA22FF')('📖 Read README'), value: 'read' },
{ name: chalk.hex('#FF69B4')('⭐ Bookmark'), value: 'bookmark' },
{ name: chalk.hex('#FFD700')('🎲 Another random'), value: 'random' },
{ name: chalk.hex('#9733EE')('🌐 Open in browser'), value: 'browser' },
{ name: chalk.gray('← Back'), value: 'back' }
]
}
]);
switch (action) {
case 'read':
if (readme) {
const viewer = require('./viewer');
await viewer.viewReadme(repo, readme);
await showRandom();
} else {
console.log(chalk.yellow('\n README not indexed\n'));
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter...' }]);
await showRandom();
}
break;
case 'bookmark':
const isBookmarked = db.isBookmarked(repo.id);
if (!isBookmarked) {
const { notes, tags } = await inquirer.prompt([
{
type: 'input',
name: 'notes',
message: 'Notes (optional):',
default: ''
},
{
type: 'input',
name: 'tags',
message: 'Tags (comma-separated):',
default: ''
}
]);
db.addBookmark(repo.id, notes, tags, '');
console.log(chalk.green('\n ✓ Bookmarked!\n'));
} else {
console.log(chalk.yellow('\n Already bookmarked!\n'));
}
await new Promise(resolve => setTimeout(resolve, 1500));
await showRandom();
break;
case 'random':
await showRandom();
break;
case 'browser':
const { spawn } = require('child_process');
spawn('xdg-open', [repo.url], { detached: true, stdio: 'ignore' });
await showRandom();
break;
case 'back':
break;
}
}
module.exports = {
showRandom
};

252
lib/search.js Normal file
View File

@@ -0,0 +1,252 @@
const inquirer = require('inquirer');
const chalk = require('chalk');
const Table = require('cli-table3');
const { purpleGold, pinkPurple, sectionHeader } = require('./banner');
const db = require('./db-operations');
// Interactive search
async function interactiveSearch() {
console.clear();
sectionHeader('SEARCH READMES', '🔍');
const { query } = await inquirer.prompt([
{
type: 'input',
name: 'query',
message: purpleGold('Enter your search query:'),
validate: (input) => {
if (!input.trim()) {
return 'Please enter a search query';
}
return true;
}
}
]);
await performSearch(query);
}
// Quick search from CLI
async function quickSearch(query, limit = 50) {
console.clear();
sectionHeader(`SEARCH RESULTS FOR "${query}"`, '🔍');
await performSearch(query, limit);
}
// Perform search and display results
async function performSearch(query, limit = 50) {
const results = db.searchReadmes(query, limit);
if (results.length === 0) {
console.log(chalk.yellow(' No results found.\n'));
console.log(chalk.gray(' Try:\n'));
console.log(chalk.gray(' • Using different keywords'));
console.log(chalk.gray(' • Building the index with: awesome index'));
console.log(chalk.gray(' • Checking for typos\n'));
return;
}
console.log(chalk.hex('#FFD700')(` Found ${results.length} results!\n`));
// Display results table
const table = new Table({
head: [
chalk.hex('#DA22FF')('#'),
chalk.hex('#DA22FF')('Name'),
chalk.hex('#DA22FF')('Description'),
chalk.hex('#DA22FF')('⭐'),
chalk.hex('#DA22FF')('Lang')
],
colWidths: [5, 25, 50, 7, 12],
wordWrap: true,
style: {
head: [],
border: ['gray']
}
});
results.slice(0, 20).forEach((result, idx) => {
table.push([
chalk.gray(idx + 1),
chalk.hex('#FF69B4')(result.name),
result.description ? result.description.substring(0, 80) : chalk.gray('No description'),
chalk.hex('#FFD700')(result.stars || '-'),
chalk.hex('#9733EE')(result.language || '-')
]);
});
console.log(table.toString());
if (results.length > 20) {
console.log(chalk.gray(`\n ... and ${results.length - 20} more results\n`));
}
// Let user select a result
const choices = results.map((result, idx) => ({
name: `${idx + 1}. ${chalk.hex('#FF69B4')(result.name)} ${chalk.gray('-')} ${result.description ? result.description.substring(0, 60) : 'No description'}`,
value: result,
short: result.name
}));
choices.push(new inquirer.Separator());
choices.push({ name: chalk.gray('← Back to menu'), value: null });
const { selected } = await inquirer.prompt([
{
type: 'list',
name: 'selected',
message: 'Select a repository:',
choices: choices,
pageSize: 15
}
]);
if (selected) {
await viewRepository(selected);
}
}
// View repository details and actions
async function viewRepository(result) {
console.clear();
const repo = db.getRepository(result.repository_id);
const readme = db.getReadme(result.repository_id);
const isBookmarked = db.isBookmarked(result.repository_id);
// Display header
console.log(purpleGold(`\n${repo.name}\n`));
console.log(chalk.gray('━'.repeat(70)));
console.log(chalk.hex('#DA22FF')(' URL: ') + chalk.cyan(repo.url));
console.log(chalk.hex('#FF69B4')(' Description:') + ` ${repo.description || chalk.gray('No description')}`);
console.log(chalk.hex('#FFD700')(' Language: ') + ` ${repo.language || chalk.gray('Unknown')}`);
console.log(chalk.hex('#9733EE')(' Stars: ') + ` ${chalk.bold(repo.stars || '0')}`);
console.log(chalk.hex('#DA22FF')(' Forks: ') + ` ${repo.forks || '0'}`);
if (repo.topics) {
const topics = repo.topics.split(',').filter(Boolean);
if (topics.length > 0) {
console.log(chalk.hex('#FF69B4')(' Topics: ') + ` ${topics.map(t => chalk.hex('#FFD700')(t)).join(', ')}`);
}
}
console.log(chalk.gray('━'.repeat(70)));
console.log();
if (isBookmarked) {
console.log(chalk.hex('#FFD700')(' ⭐ Bookmarked'));
}
// Record in history
db.addToHistory(result.repository_id);
// Actions menu
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: `${chalk.hex('#DA22FF')('📖')} Read README`, value: 'read' },
{ name: `${chalk.hex('#FF69B4')('⭐')} ${isBookmarked ? 'Remove bookmark' : 'Add bookmark'}`, value: 'bookmark' },
{ name: `${chalk.hex('#FFD700')('📝')} Add to custom list`, value: 'list' },
{ name: `${chalk.hex('#9733EE')('🌐')} Open in browser`, value: 'browser' },
{ name: `${chalk.hex('#DA22FF')('📥')} Clone repository`, value: 'clone' },
new inquirer.Separator(),
{ name: chalk.gray('← Back to search results'), value: 'back' }
]
}
]);
switch (action) {
case 'read':
if (readme) {
const viewer = require('./viewer');
await viewer.viewReadme(repo, readme);
await viewRepository(result);
} else {
console.log(chalk.yellow('\n README not indexed yet\n'));
await inquirer.prompt([{ type: 'input', name: 'continue', message: 'Press Enter to continue...' }]);
await viewRepository(result);
}
break;
case 'bookmark':
await toggleBookmark(repo);
await viewRepository(result);
break;
case 'list':
const customLists = require('./custom-lists');
await customLists.addToList(repo.id);
await viewRepository(result);
break;
case 'browser':
const { spawn } = require('child_process');
spawn('xdg-open', [repo.url], { detached: true, stdio: 'ignore' });
await viewRepository(result);
break;
case 'clone':
const checkout = require('./checkout');
await checkout.cloneRepository(repo.url);
await viewRepository(result);
break;
case 'back':
// Return to previous context
break;
}
}
// Toggle bookmark
async function toggleBookmark(repo) {
const isBookmarked = db.isBookmarked(repo.id);
if (isBookmarked) {
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Remove this bookmark?',
default: false
}
]);
if (confirm) {
db.removeBookmark(repo.id);
console.log(chalk.yellow('\n ✓ Bookmark removed\n'));
}
} else {
const { notes, tags, categories } = await inquirer.prompt([
{
type: 'input',
name: 'notes',
message: 'Add notes (optional):',
default: ''
},
{
type: 'input',
name: 'tags',
message: 'Add tags (comma-separated):',
default: ''
},
{
type: 'input',
name: 'categories',
message: 'Add categories (comma-separated):',
default: ''
}
]);
db.addBookmark(repo.id, notes, tags, categories);
console.log(chalk.green('\n ✓ Bookmarked!\n'));
}
}
module.exports = {
interactiveSearch,
quickSearch,
performSearch,
viewRepository
};

212
lib/settings.js Normal file
View File

@@ -0,0 +1,212 @@
const inquirer = require('inquirer');
const chalk = require('chalk');
const { purpleGold, sectionHeader } = require('./banner');
const db = require('./db-operations');
// Default settings
const DEFAULT_SETTINGS = {
theme: 'purple-gold',
pageSize: 15,
rateLimitDelay: 100,
autoOpenBrowser: false,
defaultBadgeStyle: 'flat',
defaultBadgeColor: 'blueviolet',
githubToken: null
};
// Manage settings
async function manage() {
console.clear();
sectionHeader('SETTINGS', '⚙️');
// Load current settings
const currentSettings = {};
Object.keys(DEFAULT_SETTINGS).forEach(key => {
currentSettings[key] = db.getSetting(key, DEFAULT_SETTINGS[key]);
});
console.log(chalk.hex('#DA22FF')(' Current Settings:\n'));
console.log(chalk.gray('━'.repeat(70)));
Object.entries(currentSettings).forEach(([key, value]) => {
let displayValue = value;
// Mask GitHub token
if (key === 'githubToken' && value && value !== 'null') {
displayValue = '***' + value.slice(-4);
}
console.log(` ${chalk.hex('#FF69B4')(key.padEnd(20))} ${chalk.hex('#FFD700')(displayValue || chalk.gray('not set'))}`);
});
console.log(chalk.gray('━'.repeat(70)));
console.log();
const oauth = require('./github-oauth');
const isAuthenticated = oauth.isAuthenticated();
const authMethod = oauth.getAuthMethod();
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{
name: isAuthenticated
? chalk.hex('#DA22FF')(`🔐 GitHub Auth (${authMethod === 'oauth' ? 'OAuth' : 'Manual'}) - Logout`)
: chalk.hex('#DA22FF')('🔐 GitHub Authentication (5000 req/hour!)'),
value: 'auth'
},
{ name: chalk.hex('#FF69B4')('✏️ Edit settings'), value: 'edit' },
{ name: chalk.hex('#FFD700')('🔄 Reset to defaults'), value: 'reset' },
{ name: chalk.hex('#9733EE')('📊 Database info'), value: 'info' },
{ name: chalk.gray('← Back'), value: 'back' }
]
}
]);
switch (action) {
case 'auth':
if (isAuthenticated) {
await oauth.logout();
} else {
const { method } = await inquirer.prompt([
{
type: 'list',
name: 'method',
message: 'Choose authentication method:',
choices: [
{ name: chalk.hex('#DA22FF')('🚀 OAuth (Recommended - Easy & Secure)'), value: 'oauth' },
{ name: chalk.hex('#FF69B4')('📝 Manual Token (Traditional)'), value: 'manual' }
]
}
]);
if (method === 'oauth') {
await oauth.authenticateWithGitHub();
} else {
await oauth.manualTokenInput();
}
}
await manage();
break;
case 'edit':
await editSettings(currentSettings);
await manage();
break;
case 'reset':
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: 'Reset all settings to defaults?',
default: false
}
]);
if (confirm) {
Object.entries(DEFAULT_SETTINGS).forEach(([key, value]) => {
db.setSetting(key, value);
});
console.log(chalk.green('\n ✓ Settings reset to defaults\n'));
await new Promise(resolve => setTimeout(resolve, 1000));
}
await manage();
break;
case 'info':
await showDatabaseInfo();
await manage();
break;
case 'back':
break;
}
}
// Edit settings
async function editSettings(currentSettings) {
console.log();
console.log(chalk.gray(' Use "GitHub Authentication" option for rate limit increase'));
console.log();
const { pageSize, rateLimitDelay, autoOpenBrowser, defaultBadgeStyle, defaultBadgeColor } = await inquirer.prompt([
{
type: 'number',
name: 'pageSize',
message: 'Page size (items per page):',
default: parseInt(currentSettings.pageSize),
validate: input => input > 0 && input <= 50 ? true : 'Must be between 1 and 50'
},
{
type: 'number',
name: 'rateLimitDelay',
message: 'Rate limit delay (ms):',
default: parseInt(currentSettings.rateLimitDelay),
validate: input => input >= 0 && input <= 5000 ? true : 'Must be between 0 and 5000'
},
{
type: 'confirm',
name: 'autoOpenBrowser',
message: 'Auto-open browser for links?',
default: currentSettings.autoOpenBrowser === 'true'
},
{
type: 'list',
name: 'defaultBadgeStyle',
message: 'Default badge style:',
choices: ['flat', 'flat-square', 'plastic', 'for-the-badge'],
default: currentSettings.defaultBadgeStyle
},
{
type: 'list',
name: 'defaultBadgeColor',
message: 'Default badge color:',
choices: ['blueviolet', 'ff69b4', 'FFD700', 'informational', 'success'],
default: currentSettings.defaultBadgeColor
}
]);
db.setSetting('pageSize', pageSize.toString());
db.setSetting('rateLimitDelay', rateLimitDelay.toString());
db.setSetting('autoOpenBrowser', autoOpenBrowser.toString());
db.setSetting('defaultBadgeStyle', defaultBadgeStyle);
db.setSetting('defaultBadgeColor', defaultBadgeColor);
console.log(chalk.green('\n ✓ Settings saved!\n'));
await new Promise(resolve => setTimeout(resolve, 1000));
}
// Show database information
async function showDatabaseInfo() {
const fs = require('fs');
const { DB_PATH } = require('./database');
console.clear();
sectionHeader('DATABASE INFO', '💾');
try {
const stats = fs.statSync(DB_PATH);
const sizeInMB = (stats.size / (1024 * 1024)).toFixed(2);
console.log(chalk.hex('#DA22FF')(' Location: ') + chalk.gray(DB_PATH));
console.log(chalk.hex('#FF69B4')(' Size: ') + chalk.hex('#FFD700')(`${sizeInMB} MB`));
console.log(chalk.hex('#9733EE')(' Modified: ') + chalk.gray(stats.mtime.toLocaleString()));
console.log();
} catch (error) {
console.log(chalk.yellow(' Database not found or inaccessible\n'));
}
await inquirer.prompt([
{
type: 'input',
name: 'continue',
message: 'Press Enter to continue...'
}
]);
}
module.exports = {
manage,
editSettings,
DEFAULT_SETTINGS
};

209
lib/shell.js Normal file
View File

@@ -0,0 +1,209 @@
const inquirer = require('inquirer');
const inquirerAutocomplete = require('inquirer-autocomplete-prompt');
const chalk = require('chalk');
const fuzzy = require('fuzzy');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { purpleGold, pinkPurple, sectionHeader } = require('./banner');
const db = require('./db-operations');
// Register autocomplete prompt
inquirer.registerPrompt('autocomplete', inquirerAutocomplete);
// Command history file
const HISTORY_FILE = path.join(os.homedir(), '.awesome', 'shell_history.txt');
let commandHistory = [];
// Load command history
function loadHistory() {
try {
if (fs.existsSync(HISTORY_FILE)) {
const content = fs.readFileSync(HISTORY_FILE, 'utf8');
commandHistory = content.split('\n').filter(Boolean);
}
} catch (error) {
commandHistory = [];
}
}
// Save command to history
function saveToHistory(command) {
if (command && !commandHistory.includes(command)) {
commandHistory.push(command);
try {
fs.appendFileSync(HISTORY_FILE, command + '\n');
} catch (error) {
// Ignore errors
}
}
}
// Available commands
const COMMANDS = {
search: 'Search through indexed READMEs',
browse: 'Browse awesome lists',
random: 'Show a random README',
stats: 'Show database statistics',
bookmarks: 'Manage bookmarks',
lists: 'Manage custom lists',
history: 'View reading history',
index: 'Rebuild the index',
settings: 'Manage settings',
help: 'Show available commands',
clear: 'Clear the screen',
exit: 'Exit the shell'
};
// Start interactive shell
async function start() {
console.clear();
console.log(purpleGold('\n📚 AWESOME INTERACTIVE SHELL 📚\n'));
console.log(chalk.gray('Type "help" for available commands, "exit" to quit\n'));
console.log(chalk.gray('━'.repeat(70)));
console.log();
loadHistory();
let running = true;
while (running) {
const { command } = await inquirer.prompt([
{
type: 'input',
name: 'command',
message: chalk.hex('#DA22FF')('awesome>'),
prefix: '',
suffix: ' '
}
]);
const trimmed = command.trim();
if (!trimmed) continue;
saveToHistory(trimmed);
const [cmd, ...args] = trimmed.split(/\s+/);
try {
running = await executeCommand(cmd.toLowerCase(), args);
} catch (error) {
console.error(chalk.red('\nError:'), error.message);
console.log();
}
}
console.log(pinkPurple('\n✨ Thanks for using Awesome! ✨\n'));
}
// Execute command
async function executeCommand(cmd, args) {
switch (cmd) {
case 'search':
if (args.length === 0) {
console.log(chalk.yellow('Usage: search <query>'));
} else {
const search = require('./search');
await search.performSearch(args.join(' '));
}
break;
case 'browse':
const browser = require('./browser');
await browser.browse();
break;
case 'random':
const random = require('./random');
await random.showRandom();
break;
case 'stats':
await showStats();
break;
case 'bookmarks':
const bookmarks = require('./bookmarks');
await bookmarks.manage();
break;
case 'lists':
const customLists = require('./custom-lists');
await customLists.manage();
break;
case 'history':
const history = require('./history');
await history.show();
break;
case 'index':
const indexer = require('./indexer');
await indexer.buildIndex();
break;
case 'settings':
const settings = require('./settings');
await settings.manage();
break;
case 'help':
showHelp();
break;
case 'clear':
console.clear();
console.log(purpleGold('\n📚 AWESOME INTERACTIVE SHELL 📚\n'));
console.log(chalk.gray('Type "help" for available commands, "exit" to quit\n'));
break;
case 'exit':
case 'quit':
return false;
default:
console.log(chalk.yellow(`Unknown command: ${cmd}`));
console.log(chalk.gray('Type "help" for available commands'));
}
console.log();
return true;
}
// Show help
function showHelp() {
console.log();
console.log(purpleGold('Available Commands:\n'));
console.log(chalk.gray('━'.repeat(70)));
Object.entries(COMMANDS).forEach(([cmd, desc]) => {
console.log(` ${chalk.hex('#FF69B4')(cmd.padEnd(12))} ${chalk.gray(desc)}`);
});
console.log(chalk.gray('━'.repeat(70)));
console.log();
}
// Show statistics
async function showStats() {
const stats = db.getStats();
console.log();
console.log(pinkPurple('📊 Database Statistics\n'));
console.log(chalk.gray('━'.repeat(70)));
console.log(chalk.hex('#DA22FF')(' Awesome Lists: ') + chalk.hex('#FFD700').bold(stats.awesomeLists));
console.log(chalk.hex('#FF69B4')(' Repositories: ') + chalk.hex('#FFD700').bold(stats.repositories));
console.log(chalk.hex('#9733EE')(' Indexed READMEs: ') + chalk.hex('#FFD700').bold(stats.readmes));
console.log(chalk.hex('#DA22FF')(' Bookmarks: ') + chalk.hex('#FFD700').bold(stats.bookmarks));
console.log(chalk.hex('#FF69B4')(' Custom Lists: ') + chalk.hex('#FFD700').bold(stats.customLists));
console.log(chalk.hex('#9733EE')(' History Items: ') + chalk.hex('#FFD700').bold(stats.historyItems));
console.log(chalk.hex('#DA22FF')(' Annotations: ') + chalk.hex('#FFD700').bold(stats.annotations));
console.log(chalk.gray('━'.repeat(70)));
}
module.exports = {
start,
executeCommand,
loadHistory,
saveToHistory
};

58
lib/stats.js Normal file
View File

@@ -0,0 +1,58 @@
const chalk = require('chalk');
const inquirer = require('inquirer');
const { purpleGold, pinkPurple, sectionHeader } = require('./banner');
const db = require('./db-operations');
// Show statistics
async function show() {
console.clear();
sectionHeader('DATABASE STATISTICS', '📊');
const stats = db.getStats();
// Main stats
console.log(pinkPurple(' 📦 INDEX OVERVIEW\n'));
console.log(chalk.hex('#DA22FF')(' Awesome Lists: ') + chalk.hex('#FFD700').bold(stats.awesomeLists));
console.log(chalk.hex('#FF69B4')(' Repositories: ') + chalk.hex('#FFD700').bold(stats.repositories));
console.log(chalk.hex('#9733EE')(' Indexed READMEs: ') + chalk.hex('#FFD700').bold(stats.readmes));
console.log();
console.log(pinkPurple(' ⭐ USER DATA\n'));
console.log(chalk.hex('#DA22FF')(' Bookmarks: ') + chalk.hex('#FFD700').bold(stats.bookmarks));
console.log(chalk.hex('#FF69B4')(' Custom Lists: ') + chalk.hex('#FFD700').bold(stats.customLists));
console.log(chalk.hex('#9733EE')(' History Items: ') + chalk.hex('#FFD700').bold(stats.historyItems));
console.log(chalk.hex('#DA22FF')(' Annotations: ') + chalk.hex('#FFD700').bold(stats.annotations));
console.log();
console.log(pinkPurple(' 🏷️ ORGANIZATION\n'));
console.log(chalk.hex('#FF69B4')(' Tags: ') + chalk.hex('#FFD700').bold(stats.tags));
console.log(chalk.hex('#9733EE')(' Categories: ') + chalk.hex('#FFD700').bold(stats.categories));
console.log();
// Calculate percentages
const indexPercentage = stats.repositories > 0
? ((stats.readmes / stats.repositories) * 100).toFixed(1)
: 0;
console.log(pinkPurple(' 📈 METRICS\n'));
console.log(chalk.hex('#DA22FF')(' Index Coverage: ') + chalk.hex('#FFD700').bold(`${indexPercentage}%`));
console.log(chalk.hex('#FF69B4')(' Bookmark Rate: ') + chalk.hex('#FFD700').bold(
stats.repositories > 0 ? `${((stats.bookmarks / stats.repositories) * 100).toFixed(2)}%` : '0%'
));
console.log();
console.log(chalk.gray('━'.repeat(70)));
console.log();
await inquirer.prompt([
{
type: 'input',
name: 'continue',
message: 'Press Enter to continue...'
}
]);
}
module.exports = {
show
};

218
lib/viewer.js Normal file
View File

@@ -0,0 +1,218 @@
const marked = require('marked');
const TerminalRenderer = require('marked-terminal');
const chalk = require('chalk');
const inquirer = require('inquirer');
const { purpleGold, goldPink } = require('./banner');
// Configure marked for terminal
marked.setOptions({
renderer: new TerminalRenderer({
heading: chalk.hex('#DA22FF').bold,
strong: chalk.hex('#FF69B4').bold,
em: chalk.hex('#FFD700').italic,
codespan: chalk.hex('#9733EE'),
code: chalk.gray,
link: chalk.cyan.underline,
list: chalk.hex('#FF69B4')
})
});
// View README with pagination
async function viewReadme(repo, readme, startLine = 0) {
const LINES_PER_PAGE = 40;
const lines = readme.raw_content.split('\n');
const totalPages = Math.ceil(lines.length / LINES_PER_PAGE);
const currentPage = Math.floor(startLine / LINES_PER_PAGE) + 1;
console.clear();
// Header
console.log(purpleGold(`\n📖 ${repo.name} - README\n`));
console.log(chalk.gray('━'.repeat(70)));
console.log(chalk.hex('#FFD700')(` Page ${currentPage} of ${totalPages}`) + chalk.gray(` | Lines ${startLine + 1}-${Math.min(startLine + LINES_PER_PAGE, lines.length)} of ${lines.length}`));
console.log(chalk.gray('━'.repeat(70)));
console.log();
// Get page content
const pageLines = lines.slice(startLine, startLine + LINES_PER_PAGE);
const pageContent = pageLines.join('\n');
// Render markdown
try {
const rendered = marked.parse(pageContent);
console.log(rendered);
} catch (error) {
// Fallback to plain text if rendering fails
console.log(pageContent);
}
console.log();
console.log(chalk.gray('━'.repeat(70)));
// Navigation menu
const choices = [];
if (startLine + LINES_PER_PAGE < lines.length) {
choices.push({ name: chalk.hex('#DA22FF')('→ Next page'), value: 'next' });
}
if (startLine > 0) {
choices.push({ name: chalk.hex('#FF69B4')('← Previous page'), value: 'prev' });
}
choices.push({ name: chalk.hex('#FFD700')('⬆ Jump to top'), value: 'top' });
choices.push({ name: chalk.hex('#9733EE')('📋 Copy URL to clipboard'), value: 'copy' });
choices.push({ name: chalk.hex('#DA22FF')('🌐 Open in browser'), value: 'browser' });
choices.push({ name: chalk.hex('#FF69B4')('✍️ Add annotation'), value: 'annotate' });
choices.push(new inquirer.Separator());
choices.push({ name: chalk.gray('← Back'), value: 'back' });
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'Navigate:',
choices: choices,
pageSize: 10
}
]);
switch (action) {
case 'next':
await viewReadme(repo, readme, startLine + LINES_PER_PAGE);
break;
case 'prev':
await viewReadme(repo, readme, Math.max(0, startLine - LINES_PER_PAGE));
break;
case 'top':
await viewReadme(repo, readme, 0);
break;
case 'copy':
// Copy URL (requires xclip or similar)
try {
const { spawn } = require('child_process');
const proc = spawn('xclip', ['-selection', 'clipboard']);
proc.stdin.write(repo.url);
proc.stdin.end();
console.log(chalk.green('\n ✓ URL copied to clipboard!\n'));
} catch (error) {
console.log(chalk.yellow('\n Install xclip to use clipboard feature\n'));
}
await new Promise(resolve => setTimeout(resolve, 1000));
await viewReadme(repo, readme, startLine);
break;
case 'browser':
const { spawn } = require('child_process');
spawn('xdg-open', [repo.url], { detached: true, stdio: 'ignore' });
await viewReadme(repo, readme, startLine);
break;
case 'annotate':
await addAnnotation(repo, readme, startLine);
await viewReadme(repo, readme, startLine);
break;
case 'back':
// Return to previous screen
break;
}
}
// Add annotation
async function addAnnotation(repo, readme, currentLine) {
const { annotationType } = await inquirer.prompt([
{
type: 'list',
name: 'annotationType',
message: 'Annotation type:',
choices: [
{ name: 'Document annotation (whole README)', value: 'document' },
{ name: 'Line annotation (specific line)', value: 'line' },
{ name: 'Cancel', value: 'cancel' }
]
}
]);
if (annotationType === 'cancel') return;
let lineNumber = null;
if (annotationType === 'line') {
const { line } = await inquirer.prompt([
{
type: 'number',
name: 'line',
message: 'Line number:',
default: currentLine + 1,
validate: (input) => {
const lines = readme.raw_content.split('\n');
if (input < 1 || input > lines.length) {
return `Line must be between 1 and ${lines.length}`;
}
return true;
}
}
]);
lineNumber = line;
}
const { content } = await inquirer.prompt([
{
type: 'input',
name: 'content',
message: 'Annotation:',
validate: (input) => input.trim() ? true : 'Annotation cannot be empty'
}
]);
// Save annotation
const dbInstance = require('./database').getDb();
const stmt = dbInstance.prepare(`
INSERT INTO annotations (repository_id, line_number, content)
VALUES (?, ?, ?)
`);
stmt.run(repo.id, lineNumber, content);
console.log(chalk.green('\n ✓ Annotation added!\n'));
await new Promise(resolve => setTimeout(resolve, 1000));
}
// View annotations for a repository
async function viewAnnotations(repoId) {
const dbInstance = require('./database').getDb();
const annotations = dbInstance.prepare(`
SELECT * FROM annotations
WHERE repository_id = ?
ORDER BY line_number ASC, created_at DESC
`).all(repoId);
if (annotations.length === 0) {
console.log(chalk.yellow('\n No annotations found\n'));
return;
}
console.log(goldPink(`\n✍️ Annotations (${annotations.length})\n`));
console.log(chalk.gray('━'.repeat(70)));
annotations.forEach((ann, idx) => {
console.log();
console.log(chalk.hex('#DA22FF')(` ${idx + 1}. `) +
(ann.line_number ? chalk.hex('#FFD700')(`Line ${ann.line_number}`) : chalk.hex('#FF69B4')('Document')));
console.log(chalk.gray(` ${ann.content}`));
console.log(chalk.gray(` ${new Date(ann.created_at).toLocaleString()}`));
});
console.log();
console.log(chalk.gray('━'.repeat(70)));
console.log();
}
module.exports = {
viewReadme,
viewAnnotations,
addAnnotation
};