UNPKG

itap-cli-demo

Version:

> A lightweight, AI-powered npm package finder — directly from your terminal.

270 lines (269 loc) 9.71 kB
import { select } from "@inquirer/prompts"; import figlet from "figlet"; import gradient from "gradient-string"; import fetch from "node-fetch"; import { spawn } from "child_process"; import { getKey, register, rotate } from "./auth.js"; import ora from "ora"; const API_URL = "https://itap.onrender.com"; let key = await getKey(); if (!key) key = await register(); async function fetchJsonAuthed(path, init = {}, canRetry = true) { init.headers = { "Content-Type": "application/json", ...(init.headers || {}), "x-api-key": key, }; const res = await fetch(`${API_URL}${path}`, init); if (res.ok) { const t = await res.text(); return t ? JSON.parse(t) : null; } const raw = await res.text().catch(() => ""); let payload; try { payload = JSON.parse(raw); } catch { } const code = payload?.error_code; if (res.status === 401 && code === "expired" && canRetry) { const fresh = await rotate(key); key = fresh; init.headers["x-api-key"] = fresh; return fetchJsonAuthed(path, init, false); } if ((res.status === 401 || res.status === 429) && code === "exhausted") { const until = payload?.expires_at ? ` (resets ${new Date(payload.expires_at).toLocaleString()})` : ""; console.error(`❌ Quota exhausted${until}. Please wait until expiry.`); process.exit(1); } if (res.status === 401 && code === "invalid" && canRetry) { const fresh = await register(); key = fresh; init.headers["x-api-key"] = fresh; return fetchJsonAuthed(path, init, false); } console.error(payload?.error || raw || `${res.status} ${res.statusText}`); process.exit(1); } /** ---------- utils ---------- */ const CHUNK_SIZE = 4; const sortLabels = { downloads: "downloads", stars: "stars", lastUpdated: "last updated", itapScore: "itap™ score", }; function makeHyperlink(text, url) { return `\u001b]8;;${url}\u0007${text}\u001b]8;;\u0007`; } function formatNumber(n) { return Intl.NumberFormat("en-US", { notation: "compact" }).format(n); } function formatDate(iso) { const d = new Date(iso); if (Number.isNaN(d.getTime())) return "Unknown"; return new Intl.DateTimeFormat("en-US", { year: "numeric", month: "short", day: "numeric" }).format(d); } function sortPackages(by) { return (a, b) => { switch (by) { case "downloads": return b.downloads - a.downloads; case "stars": return b.stars - a.stars; case "lastUpdated": return new Date(b.lastUpdated).getTime() - new Date(a.lastUpdated).getTime(); case "itapScore": return b.score - a.score; } }; } function buildChoice(pkg) { const starsPart = pkg.repo ? `⭐ ${formatNumber(pkg.stars)} ` : ""; const repoPart = pkg.repo ? makeHyperlink("Repo", pkg.repo) : "No repo"; return { value: pkg.name, name: `📦 ${pkg.name}${pkg.description}\n` + ` 🔗 ${repoPart} ` + `📈 ${formatNumber(pkg.downloads)} ` + starsPart + `🕒 ${formatDate(pkg.lastUpdated)}`, }; } /** ---------- API wrappers ---------- */ export async function searchVectorDb(query) { const data = await fetchJsonAuthed("/search", { method: "POST", body: JSON.stringify({ query }), }); return data.results.map((pkg) => ({ ...pkg, lastUpdated: pkg.lastUpdated || "Unknown", skips: pkg.skips || 0, selects: pkg.selects || 0, score: pkg.score || 0, })); } async function storePackages(packages) { await fetchJsonAuthed("/store-batch", { method: "POST", body: JSON.stringify({ packages }), }); } async function updatePackageScores(packages, selectedName) { await fetchJsonAuthed("/update-scores", { method: "POST", body: JSON.stringify({ packages, selectedName }), }); } /** ---------- NPM helpers ---------- */ export async function searchNPM(query, numChunk = 0) { const params = new URLSearchParams({ text: query, size: String(CHUNK_SIZE), from: String(numChunk * CHUNK_SIZE) }); const fullUrl = `https://registry.npmjs.com/-/v1/search?${params}`; try { const res = await fetch(fullUrl); if (!res.ok) throw new Error(`NPM registry error: ${res.status}`); const data = (await res.json()); return Promise.all(data.objects.map(async (obj) => { const repo = obj.package.links?.repository || obj.package.links?.homepage || ""; const stars = repo ? await getGithubStars(repo) : 0; return { name: obj.package.name, version: obj.package.version, description: obj.package.description || "No description available", repo, downloads: obj.downloads?.weekly ?? 0, lastUpdated: obj.updated || "Unknown", stars, score: 0, }; })); } catch (err) { console.error("Error searching for package:", err.message); return []; } } async function getGithubStars(repoUrl) { if (!repoUrl || !repoUrl.includes("github.com")) return 0; try { const m = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/); if (!m) return 0; const [, owner, repo] = m; const cleanRepo = repo.replace(/\.git$/, ""); const apiUrl = `https://api.github.com/repos/${owner}/${cleanRepo}`; const res = await fetch(apiUrl); if (!res.ok) throw new Error(`GitHub API error: ${res.status}`); const json = await res.json(); return json.stargazers_count || 0; } catch (e) { console.error("Error fetching GitHub stars:", e.message); return 0; } } /** ---------- CLI ops ---------- */ export async function handleQuery(query, sortArg = "stars") { printBanner(); const results = []; let numChunk = 0; let resultsSource = null; let selected = false; let sortBy = sortArg; const spinner = ora("Searching for packages...").start(); try { const vectorSearchResults = await searchVectorDb(query); if (vectorSearchResults.length > 0) { results.push(...vectorSearchResults); resultsSource = "vector"; } else { const npmSearchResults = await searchNPM(query); if (npmSearchResults.length > 0) { results.push(...npmSearchResults); resultsSource = "npm"; } else { spinner.fail("No packages found."); return; } } spinner.succeed(`Found ${results.length} packages`); } catch (e) { spinner.fail(e?.message ?? "Search failed"); throw e; } while (!selected) { results.sort(sortPackages(sortBy)); const selection = (await select({ message: `Select a package (sorted by ${sortLabels[sortBy]}) — page ${numChunk + 1}`, choices: [ ...results.map(buildChoice), { value: "sort", name: `🔃 Change sort order (current: ${sortLabels[sortBy]})` }, { value: "search", name: "🔍 Continue the search!" }, { value: "exit", name: "❌ Exit" }, ], }).catch((err) => { if (err.name === "ExitPromptError") { console.log("goodbye."); process.exit(0); } })); switch (selection) { case "exit": console.log("Exiting the search."); process.exit(0); case "search": numChunk++; results.length = 0; results.push(...(await searchNPM(query, numChunk))); break; case "sort": { const newSort = (await select({ message: "Sort by:", choices: [ { name: "📥 Most Downloads", value: "downloads" }, { name: "⭐ Most Stars", value: "stars" }, { name: "🕓 Recently Updated", value: "lastUpdated" }, { name: "💡 itap™ Score", value: "itapScore" }, ], })); sortBy = newSort; break; } default: { if (resultsSource === "npm") await storePackages(results); selected = true; console.log("Installing package..."); await updatePackageScores(results, selection); await installPackage(selection); } } } } export function installPackage(pkgName) { return new Promise((resolve, reject) => { const proc = spawn("npm", ["install", pkgName], { stdio: "inherit", shell: true }); proc.on("close", (code) => (code === 0 ? resolve() : reject(new Error(`npm install exited with code ${code}`)))); proc.on("error", reject); }); } export function printBanner() { const pastel = gradient(["#FFD6EC", "#C5F3FF", "#D9F8C4", "#FFFACD"]); figlet.text("itap", { font: "Big", horizontalLayout: "default", verticalLayout: "default" }, (err, data) => { if (err || !data) { console.error("figlet error:", err); return; } console.clear(); console.log("\n" + pastel(data)); console.log(pastel(" is there a package?\n")); }); }