import { Command } from "commander"; import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; import chalk from "chalk"; import ora from "ora"; import { apiRequest } from "../lib/api-client.js"; import type { SkillSummary } from "@skills-hub-ai/shared"; /** * Read dependency names from common project files in the current directory. */ function detectDeps(cwd: string): string[] { const deps: string[] = []; // package.json (Node.js) const pkgPath = join(cwd, "package.json"); if (existsSync(pkgPath)) { try { const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); if (pkg.dependencies) deps.push(...Object.keys(pkg.dependencies)); if (pkg.devDependencies) deps.push(...Object.keys(pkg.devDependencies)); } catch { /* skip malformed */ } } // requirements.txt (Python) const reqPath = join(cwd, "requirements.txt"); if (existsSync(reqPath)) { try { const lines = readFileSync(reqPath, "utf-8").split("\n"); for (const line of lines) { const name = line .trim() .split(/[>=", "Number of suggestions", "10") .action(async (options) => { const spinner = ora("Scanning project dependencies...").start(); try { const deps = detectDeps(process.cwd()); if (deps.length === 0) { spinner.warn( "No dependency files found (package.json, requirements.txt, Gemfile, go.mod, Cargo.toml, pubspec.yaml)", ); return; } // Limit to top 50 deps to avoid URL/payload size issues const cappedDeps = deps.slice(0, 50); spinner.text = `Found ${deps.length} dependencies${deps.length > 50 ? ` (sending top 50)` : ""}. Finding matching skills...`; // POST /search/suggest-by-deps returned a bare array until 2026-07-30 and // a {data,cursor,hasMore} envelope after. Accept BOTH shapes rather than // just the new one: every CLI already published to npm ships the old // unwrapping, so a version that only understands the envelope would break // the moment it met an older self-hosted API, and one that only // understands the array crashes with "skills is not iterable" against // the current one. const response = await apiRequest( "/api/v1/search/suggest-by-deps", { method: "POST", body: JSON.stringify({ deps: cappedDeps, limit: parseInt(options.limit, 10), }), }, ); const skills = unwrapSkillList(response); if (skills.length === 0) { spinner.info("No matching skills found for your project dependencies."); return; } spinner.succeed(`Found ${skills.length} skills for your stack:\n`); for (const skill of skills) { const score = skill.qualityScore !== null ? chalk.green(`[${skill.qualityScore}]`) : chalk.gray("[--]"); console.log( ` ${score} ${chalk.bold(skill.name)} ${chalk.gray(`v${skill.latestVersion}`)}`, ); console.log(` ${chalk.gray(skill.description.slice(0, 80))}`); console.log( ` ${chalk.cyan(`npx @skills-hub-ai/cli install ${skill.slug}`)} ${chalk.gray(`${skill.installCount} installs`)}`, ); console.log(); } } catch (err) { spinner.fail( chalk.red(err instanceof Error ? err.message : "Suggestion failed"), ); process.exit(1); } });