a new start
This commit is contained in:
98
lib/banner.js
Normal file
98
lib/banner.js
Normal 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
196
lib/bookmarks.js
Normal 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
212
lib/browser.js
Normal 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
97
lib/checkout.js
Normal 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
414
lib/custom-lists.js
Normal 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 += `\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 += ` `;
|
||||
}
|
||||
if (item.language) {
|
||||
content += ` `;
|
||||
}
|
||||
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
239
lib/database.js
Normal 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
260
lib/db-operations.js
Normal 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
218
lib/github-api.js
Normal 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
261
lib/github-oauth.js
Normal 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
118
lib/history.js
Normal 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
284
lib/indexer.js
Normal 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
109
lib/menu.js
Normal 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
101
lib/random.js
Normal 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
252
lib/search.js
Normal 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
212
lib/settings.js
Normal 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
209
lib/shell.js
Normal 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
58
lib/stats.js
Normal 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
218
lib/viewer.js
Normal 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
|
||||
};
|
||||
Reference in New Issue
Block a user