UNPKG

browse

Version:

Unified Browserbase CLI for browser automation and cloud APIs.

184 lines (183 loc) 7.07 kB
import { fail } from "../errors.js"; import { outputTable } from "../output.js"; import { defaultSkillsApiBaseUrl, isRecord, responseDetail } from "./shared.js"; export async function listCatalogSkills(options = {}) { const payload = await requestSkillsJson(skillsCatalogApiUrl(options.query)); return parseSkillsResponse(payload); } export function prioritizeExactSkillMatch(skills, query) { const normalizedQuery = query.toLowerCase(); return [...skills].sort((a, b) => { const aExact = a.slug.toLowerCase() === normalizedQuery; const bExact = b.slug.toLowerCase() === normalizedQuery; if (aExact === bExact) return 0; return aExact ? -1 : 1; }); } export function outputSkillTable(skills, options = {}) { const visibleSkills = skills.slice(0, options.limit ?? skills.length); if (visibleSkills.length === 0) { console.log("No skills found."); return; } if (options.heading) { console.log(`${options.heading} (${skills.length})`); } outputTable(visibleSkills, [ { header: "Skill", maxWidth: 42, value: (skill) => skill.slug, }, { header: "Title", maxWidth: 42, value: (skill) => skill.title, }, { header: "Method", maxWidth: 7, value: (skill) => skill.recommendedMethod, }, { align: "right", header: "Installs", maxWidth: 8, value: (skill) => skill.installCount, }, { header: "Tags", maxWidth: 36, value: (skill) => formatList(skill.tags), }, ], { wide: options.wide }); console.log("Install with: browse skills add <skill>"); if (visibleSkills.length < skills.length) { console.log(`Showing ${visibleSkills.length} of ${skills.length} skills. Use --limit, --all, --wide, or --json.`); } else { console.log("Use --wide for full values or --json for full descriptions."); } } export function printSkillDetail(skill) { console.log(skill.title); console.log(`Skill: ${skill.slug}`); console.log(`Method: ${skill.recommendedMethod}`); console.log(`Source: ${skill.source || "-"}`); console.log(`Installs: ${skill.installCount}`); console.log(`Tags: ${formatList(skill.tags)}`); if (skill.description) { console.log(`\n${skill.description}`); } console.log(`\nInstall: browse skills add ${skill.slug}`); } export function exactSkillMatch(skills, query) { const normalizedQuery = query.toLowerCase(); return skills.find((skill) => skill.slug.toLowerCase() === normalizedQuery); } function formatList(values) { return values.length > 0 ? values.join(", ") : "-"; } function skillsCatalogApiUrl(query) { const baseUrl = process.env.BROWSE_SKILLS_API_BASE_URL || defaultSkillsApiBaseUrl; const url = new URL("api/skills", baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`); if (query) { url.searchParams.set("q", query); } const bypassToken = process.env.BROWSE_ALPHA_TOKEN; if (bypassToken && !url.searchParams.has("x-vercel-protection-bypass")) { url.searchParams.append("x-vercel-protection-bypass", bypassToken); } return url; } async function requestSkillsJson(url) { let response; try { response = await fetch(url, { headers: { accept: "application/json", }, }); } catch (error) { fail(`Failed to fetch skills: ${error instanceof Error ? error.message : String(error)}`); } if (!response.ok) { fail(`Failed to fetch skills: ${response.status} ${response.statusText}${await responseDetail(response)}`); } try { return await response.json(); } catch (error) { fail(`Failed to parse skills response: ${error instanceof Error ? error.message : String(error)}`); } } function parseSkillsResponse(payload) { if (!isRecord(payload) || !Array.isArray(payload.skills)) { fail('Invalid skills response: expected {"skills":[...]}.'); } return payload.skills.map((skill, index) => parseSkill(skill, `skills[${index}]`)); } function parseSkill(payload, context) { if (!isRecord(payload)) { fail(`Invalid skills response for ${context}: skill must be an object.`); } return { hostname: requiredString(payload.hostname, context, "hostname"), task: requiredString(payload.task, context, "task"), slug: requiredString(payload.slug, context, "slug"), name: stringField(payload.name, context, "name"), title: stringField(payload.title, context, "title"), description: stringField(payload.description, context, "description"), category: stringField(payload.category, context, "category"), aliases: stringArrayField(payload.aliases, context, "aliases"), tags: stringArrayField(payload.tags, context, "tags"), source: stringField(payload.source, context, "source"), updated: stringField(payload.updated, context, "updated"), recommendedMethod: methodField(payload.recommendedMethod, context, "recommendedMethod"), verified: booleanField(payload.verified, context, "verified"), proxies: booleanField(payload.proxies, context, "proxies"), sourceUrl: stringField(payload.sourceUrl, context, "sourceUrl"), partner: booleanField(payload.partner, context, "partner"), screenshotUrls: stringArrayField(payload.screenshotUrls, context, "screenshotUrls"), installCount: numberField(payload.installCount, context, "installCount"), }; } function requiredString(value, context, field) { if (typeof value !== "string" || value.length === 0) { fail(`Invalid skills response for ${context}: ${field} must be a non-empty string.`); } return value; } function stringField(value, context, field) { if (typeof value !== "string") { fail(`Invalid skills response for ${context}: ${field} must be a string.`); } return value; } function methodField(value, context, field) { if (value !== "api" && value !== "browser") { fail(`Invalid skills response for ${context}: ${field} must be "api" or "browser".`); } return value; } function booleanField(value, context, field) { if (typeof value !== "boolean") { fail(`Invalid skills response for ${context}: ${field} must be a boolean.`); } return value; } function numberField(value, context, field) { if (typeof value !== "number" || !Number.isFinite(value)) { fail(`Invalid skills response for ${context}: ${field} must be a number.`); } return value; } function stringArrayField(value, context, field) { if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) { fail(`Invalid skills response for ${context}: ${field} must be an array of strings.`); } return value; }