Files
jackwener__opencli/clis/npm/utils.js
jakevin 55088bbb28 feat: 13 read adapters across 5 new sites + 4 extensions (round 3) (#1346)
New sites (8 commands):
- npm    : search / package / downloads (registry.npmjs.org + api.npmjs.org)
- pypi   : package / downloads (pypi.org + pypistats.org)
- crates : search / crate (crates.io)
- mdn    : search (developer.mozilla.org)
- nvd    : cve (services.nvd.nist.gov)

Extensions (5 commands; +1 dblp/author surfaced in index):
- hf            : spaces (Hugging Face Spaces by likes / created_at / last_modified)
- dblp          : venue (search dblp's venue registry by acronym/topic)
- coingecko     : derivatives (perpetual / futures markets, 24h volume)
- stackoverflow : related (related questions for a given question id)

All commands hit public unauthenticated endpoints (Strategy.PUBLIC, browser:false),
typed-fail-fast on bad inputs (no silent fallback / clamp), and round-trip listing
ids into their detail commands where applicable.

Audits (all green vs baseline):
- typed-error-lint        : 196 = 196 baseline, no new
- silent-column-drop      : 103 = 103 baseline, no new
- listing-id-pairing      : 13 advisory (was 12; +1 = dblp/venue with no
                            corresponding venue-detail command)

Doc coverage : 120/120 adapter dirs documented (+5 new doc pages, +4 updated)
Manifest     : 722 entries (was 709; +13 commands)

Live verified:
- npm search react / npm package react / npm downloads react --period last-week
- npm downloads react --period 2025-01-01:2025-01-05
- pypi package requests / pypi downloads requests --period recent / overall
- crates search tokio / crates crate serde
- mdn search fetch
- nvd cve CVE-2021-44228
- hf spaces --limit 3
- dblp venue ICLR
- coingecko derivatives --limit 3
- stackoverflow related 79935770 --limit 3
- typed-error sanity: invalid CVE id, bad npm name, bad --period
2026-05-06 13:14:41 +08:00

77 lines
2.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Shared helpers for the npm adapters that hit the public npm registry
// (registry.npmjs.org) and download stats API (api.npmjs.org).
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
export const NPM_REGISTRY = 'https://registry.npmjs.org';
export const NPM_API = 'https://api.npmjs.org';
const UA = 'opencli-npm-adapter (+https://github.com/jackwener/opencli)';
// npm package names: 1-214 chars, lowercase letters/numbers/-._ , scoped form `@scope/name`.
const PKG_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
export function requireString(value, label) {
const s = String(value ?? '').trim();
if (!s) throw new ArgumentError(`npm ${label} cannot be empty`);
return s;
}
export function requirePackageName(value) {
const s = String(value ?? '').trim();
if (!s) throw new ArgumentError('npm package name is required (e.g. "react", "@vercel/og")');
if (s.length > 214) {
throw new ArgumentError(`npm package name "${value}" is too long (max 214 chars)`);
}
if (!PKG_NAME.test(s)) {
throw new ArgumentError(
`npm package name "${value}" is not a valid registry name`,
'Names are 1–214 chars of lowercase a-z / 0-9 / "-._" (scoped form: "@scope/name").',
);
}
return s;
}
export function requireBoundedInt(value, defaultValue, maxValue, label = 'limit') {
const raw = value ?? defaultValue;
const n = typeof raw === 'number' ? raw : Number(raw);
if (!Number.isInteger(n) || n <= 0) {
throw new ArgumentError(`npm ${label} must be a positive integer`);
}
if (n > maxValue) {
throw new ArgumentError(`npm ${label} must be <= ${maxValue}`);
}
return n;
}
export async function npmFetch(url, label) {
let resp;
try {
resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });
}
catch (err) {
throw new CommandExecutionError(
`${label} request failed: ${err?.message ?? err}`,
'Check that registry.npmjs.org / api.npmjs.org are reachable from this network.',
);
}
if (resp.status === 404) {
throw new EmptyResultError(label, `npm registry returned 404 for ${url}.`);
}
if (resp.status === 429) {
throw new CommandExecutionError(
`${label} returned HTTP 429 (rate limited)`,
'npm throttles unauthenticated bursts; wait a few seconds and retry.',
);
}
if (!resp.ok) {
throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
}
let body;
try {
body = await resp.json();
}
catch (err) {
throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
}
return body;
}