diff --git a/cli-manifest.json b/cli-manifest.json index aa08cd26c..eb9817d0c 100644 --- a/cli-manifest.json +++ b/cli-manifest.json @@ -18547,6 +18547,163 @@ "modulePath": "oeis/sequence.js", "sourceFile": "oeis/sequence.js" }, + { + "site": "ohpm", + "name": "dependents", + "description": "List packages that depend on an OHPM package", + "access": "read", + "domain": "ohpm.openharmony.cn", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "name", + "type": "str", + "required": true, + "positional": true, + "help": "OHPM package name (e.g. \"@ohos/axios\")" + }, + { + "name": "version", + "type": "string", + "required": false, + "help": "Package version; omit for latest" + }, + { + "name": "limit", + "type": "int", + "default": 20, + "required": false, + "help": "Max dependents (1-50)" + } + ], + "columns": [ + "rank", + "name", + "version", + "dependent", + "url" + ], + "type": "js", + "modulePath": "ohpm/dependents.js", + "sourceFile": "ohpm/dependents.js" + }, + { + "site": "ohpm", + "name": "keywords", + "description": "List hot OHPM search keywords", + "access": "read", + "domain": "ohpm.openharmony.cn", + "strategy": "public", + "browser": false, + "args": [], + "columns": [ + "rank", + "keyword" + ], + "type": "js", + "modulePath": "ohpm/keywords.js", + "sourceFile": "ohpm/keywords.js" + }, + { + "site": "ohpm", + "name": "package", + "description": "Single OHPM package metadata (version, downloads, license, repository)", + "access": "read", + "domain": "ohpm.openharmony.cn", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "name", + "type": "str", + "required": true, + "positional": true, + "help": "OHPM package name (e.g. \"@ohos/axios\")" + }, + { + "name": "version", + "type": "string", + "required": false, + "help": "Package version; omit for latest" + } + ], + "columns": [ + "name", + "version", + "description", + "license", + "downloads", + "likes", + "points", + "popularity", + "fileSize", + "fileCount", + "repository", + "keywords", + "publisher", + "org", + "dependencies", + "devDependencies", + "dependents", + "versions", + "published", + "url" + ], + "type": "js", + "modulePath": "ohpm/package.js", + "sourceFile": "ohpm/package.js" + }, + { + "site": "ohpm", + "name": "search", + "description": "Search OpenHarmony OHPM third-party packages by keyword", + "access": "read", + "domain": "ohpm.openharmony.cn", + "strategy": "public", + "browser": false, + "args": [ + { + "name": "query", + "type": "str", + "required": true, + "positional": true, + "help": "Package keyword (e.g. \"axios\", \"json\")" + }, + { + "name": "limit", + "type": "int", + "default": 20, + "required": false, + "help": "Max results (1-50)" + }, + { + "name": "sort", + "type": "string", + "default": "relevancy", + "required": false, + "help": "Sort: relevancy, likes, latest" + } + ], + "columns": [ + "rank", + "name", + "latestVersion", + "description", + "license", + "keywords", + "likes", + "points", + "popularity", + "publisher", + "org", + "published", + "url" + ], + "type": "js", + "modulePath": "ohpm/search.js", + "sourceFile": "ohpm/search.js" + }, { "site": "ones", "name": "login", diff --git a/clis/ohpm/dependents.js b/clis/ohpm/dependents.js new file mode 100644 index 000000000..7a0c7d7e0 --- /dev/null +++ b/clis/ohpm/dependents.js @@ -0,0 +1,51 @@ +// ohpm dependents โ€” list packages that depend on an OHPM package. +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { EmptyResultError } from '@jackwener/opencli/errors'; +import { + OHPM_API, + normalizeText, + ohpmFetch, + packageUrl, + requireBoundedInt, + requirePackageName, +} from './utils.js'; + +cli({ + site: 'ohpm', + name: 'dependents', + access: 'read', + description: 'List packages that depend on an OHPM package', + domain: 'ohpm.openharmony.cn', + strategy: Strategy.PUBLIC, + browser: false, + args: [ + { name: 'name', positional: true, required: true, help: 'OHPM package name (e.g. "@ohos/axios")' }, + { name: 'version', type: 'string', required: false, help: 'Package version; omit for latest' }, + { name: 'limit', type: 'int', default: 20, help: 'Max dependents (1-50)' }, + ], + columns: ['rank', 'name', 'version', 'dependent', 'url'], + func: async (args) => { + const name = requirePackageName(args.name); + const version = normalizeText(args.version); + const limit = requireBoundedInt(args.limit, 20, 50); + const path = version + ? `${encodeURIComponent(name)}/${encodeURIComponent(version)}` + : encodeURIComponent(name); + const body = await ohpmFetch(`${OHPM_API}/v1/detail/${path}`, `ohpm dependents ${name}`); + const item = body?.body; + if (!item?.name) { + throw new EmptyResultError('ohpm dependents', `OHPM returned no metadata for "${name}".`); + } + const rows = Array.isArray(item.dependent?.rows) ? item.dependent.rows : []; + if (!rows.length) { + throw new EmptyResultError('ohpm dependents', `OHPM returned no dependents for "${name}".`); + } + return rows.slice(0, limit).map((dependent, i) => ({ + rank: i + 1, + name: normalizeText(item.name), + version: normalizeText(item.version), + dependent: normalizeText(dependent), + url: packageUrl(item.name), + })); + }, +}); diff --git a/clis/ohpm/keywords.js b/clis/ohpm/keywords.js new file mode 100644 index 000000000..45897fa01 --- /dev/null +++ b/clis/ohpm/keywords.js @@ -0,0 +1,27 @@ +// ohpm keywords โ€” current hot search terms shown on the OHPM home page. +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { EmptyResultError } from '@jackwener/opencli/errors'; +import { OHPM_API, normalizeText, ohpmFetch } from './utils.js'; + +cli({ + site: 'ohpm', + name: 'keywords', + access: 'read', + description: 'List hot OHPM search keywords', + domain: 'ohpm.openharmony.cn', + strategy: Strategy.PUBLIC, + browser: false, + args: [], + columns: ['rank', 'keyword'], + func: async () => { + const body = await ohpmFetch(`${OHPM_API}/v1/frequency`, 'ohpm keywords'); + const list = Array.isArray(body?.body) ? body.body : []; + if (!list.length) { + throw new EmptyResultError('ohpm keywords', 'OHPM returned no hot keywords.'); + } + return list.map((keyword, i) => ({ + rank: i + 1, + keyword: normalizeText(keyword), + })); + }, +}); diff --git a/clis/ohpm/ohpm.test.js b/clis/ohpm/ohpm.test.js new file mode 100644 index 000000000..3e3f61f58 --- /dev/null +++ b/clis/ohpm/ohpm.test.js @@ -0,0 +1,151 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { getRegistry } from '@jackwener/opencli/registry'; +import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors'; +import { requirePackageName, requireSort } from './utils.js'; +import './search.js'; +import './package.js'; +import './dependents.js'; +import './keywords.js'; + +function jsonResponse(body, init = {}) { + return new Response(JSON.stringify(body), { + status: 200, + headers: { 'content-type': 'application/json' }, + ...init, + }); +} + +afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); +}); + +describe('ohpm shared argument validation', () => { + it('accepts scoped and unscoped package names but rejects malformed values', () => { + expect(requirePackageName('@ohos/axios')).toBe('@ohos/axios'); + expect(requirePackageName('mobileukey')).toBe('mobileukey'); + expect(() => requirePackageName('')).toThrow(ArgumentError); + expect(() => requirePackageName('@bad')).toThrow(ArgumentError); + expect(() => requirePackageName('bad space')).toThrow(ArgumentError); + }); + + it('normalizes supported sort aliases and rejects unsupported API sort keys', () => { + expect(requireSort(undefined)).toBe('relevancy'); + expect(requireSort('popular')).toBe('likes'); + expect(requireSort('newest')).toBe('latest'); + expect(() => requireSort('download')).toThrow(ArgumentError); + }); +}); + +describe('ohpm search adapter', () => { + const cmd = getRegistry().get('ohpm/search'); + + it('maps OHPM search rows into stable output columns', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(jsonResponse({ + code: 200, + body: { + rows: [{ + name: '@ohos/axios', + latestVersion: '2.2.10', + description: 'HTTP client', + license: 'MIT', + keywords: ['request', 'http'], + likes: 328, + points: 25, + popularity: 8631, + publisherName: 'SettZhao', + org: 'ohos', + latestPublishTime: 1779348839684, + }], + }, + }))); + + const rows = await cmd.func({ query: 'axios', limit: 1, sort: 'popular' }); + + expect(rows).toEqual([{ + rank: 1, + name: '@ohos/axios', + latestVersion: '2.2.10', + description: 'HTTP client', + license: 'MIT', + keywords: 'request, http', + likes: 328, + points: 25, + popularity: 8631, + publisher: 'SettZhao', + org: 'ohos', + published: '2026-05-21', + url: 'https://ohpm.openharmony.cn/#/cn/detail/%40ohos%2Faxios', + }]); + }); + + it('maps API errors to CommandExecutionError and empty rows to EmptyResultError', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(jsonResponse({ code: 217008, message: 'Invalid sortedType!' }, { status: 400 }))); + await expect(cmd.func({ query: 'axios' })).rejects.toThrow(CommandExecutionError); + + vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce(jsonResponse({ code: 200, body: { rows: [] } }))); + await expect(cmd.func({ query: 'no-such-package' })).rejects.toThrow(EmptyResultError); + }); +}); + +describe('ohpm package adapter', () => { + const cmd = getRegistry().get('ohpm/package'); + + it('fills missing detail description from search metadata for latest package lookup', async () => { + const fetchMock = vi.fn() + .mockResolvedValueOnce(jsonResponse({ + code: 200, + body: { + name: '@ohos/axios', + version: '2.2.10', + description: '', + license: 'MIT', + downloads: 183168, + keywords: ['request'], + publishTime: 1779348839684, + dependent: { total: 55 }, + versions: { '2.2.10': 1779348839684 }, + }, + })) + .mockResolvedValueOnce(jsonResponse({ + code: 200, + body: { + rows: [{ name: '@ohos/axios', description: 'Axios for OpenHarmony' }], + }, + })); + vi.stubGlobal('fetch', fetchMock); + + const rows = await cmd.func({ name: '@ohos/axios' }); + + expect(rows[0].description).toBe('Axios for OpenHarmony'); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); +}); + +describe('ohpm dependents and keywords adapters', () => { + it('returns one row per dependent with a caller supplied limit', async () => { + const cmd = getRegistry().get('ohpm/dependents'); + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(jsonResponse({ + code: 200, + body: { + name: '@ohos/axios', + version: '2.2.10', + dependent: { rows: ['a', 'b', 'c'] }, + }, + }))); + + const rows = await cmd.func({ name: '@ohos/axios', limit: 2 }); + + expect(rows.map((row) => row.dependent)).toEqual(['a', 'b']); + }); + + it('maps hot keywords to ranked rows', async () => { + const cmd = getRegistry().get('ohpm/keywords'); + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(jsonResponse({ code: 200, body: ['axios', 'json'] }))); + + await expect(cmd.func({})).resolves.toEqual([ + { rank: 1, keyword: 'axios' }, + { rank: 2, keyword: 'json' }, + ]); + }); +}); diff --git a/clis/ohpm/package.js b/clis/ohpm/package.js new file mode 100644 index 000000000..4503b4ea5 --- /dev/null +++ b/clis/ohpm/package.js @@ -0,0 +1,86 @@ +// ohpm package โ€” fetch a single OpenHarmony OHPM package's metadata. +// +// Hits `oh-package/openapi/v1/detail//`. The latest version is +// returned when --version is omitted. +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { EmptyResultError } from '@jackwener/opencli/errors'; +import { + OHPM_API, + dateFromMs, + normalizeText, + ohpmFetch, + packageUrl, + requirePackageName, +} from './utils.js'; + +function versionCount(versions) { + return versions && typeof versions === 'object' ? Object.keys(versions).length : 0; +} + +async function findLatestDescription(name) { + const params = new URLSearchParams({ + condition: name, + pageNum: '1', + pageSize: '10', + sortedType: 'relevancy', + isHomePage: 'false', + }); + const body = await ohpmFetch(`${OHPM_API}/v1/search?${params}`, `ohpm package ${name} search metadata`); + const rows = Array.isArray(body?.body?.rows) ? body.body.rows : []; + const exact = rows.find((row) => normalizeText(row.name) === name); + return normalizeText(exact?.description); +} + +cli({ + site: 'ohpm', + name: 'package', + access: 'read', + description: 'Single OHPM package metadata (version, downloads, license, repository)', + domain: 'ohpm.openharmony.cn', + strategy: Strategy.PUBLIC, + browser: false, + args: [ + { name: 'name', positional: true, required: true, help: 'OHPM package name (e.g. "@ohos/axios")' }, + { name: 'version', type: 'string', required: false, help: 'Package version; omit for latest' }, + ], + columns: [ + 'name', 'version', 'description', 'license', 'downloads', 'likes', 'points', + 'popularity', 'fileSize', 'fileCount', 'repository', 'keywords', 'publisher', + 'org', 'dependencies', 'devDependencies', 'dependents', 'versions', 'published', 'url', + ], + func: async (args) => { + const name = requirePackageName(args.name); + const version = normalizeText(args.version); + const path = version + ? `${encodeURIComponent(name)}/${encodeURIComponent(version)}` + : encodeURIComponent(name); + const body = await ohpmFetch(`${OHPM_API}/v1/detail/${path}`, `ohpm package ${name}`); + const item = body?.body; + if (!item?.name) { + throw new EmptyResultError('ohpm package', `OHPM returned no metadata for "${name}".`); + } + const description = normalizeText(item.description) || (!version ? await findLatestDescription(name) : ''); + return [{ + name: normalizeText(item.name), + version: normalizeText(item.version), + description, + license: normalizeText(item.license), + downloads: item.downloads != null ? Number(item.downloads) : null, + likes: item.likes != null ? Number(item.likes) : null, + points: item.points != null ? Number(item.points) : null, + popularity: item.popularity != null ? Number(item.popularity) : null, + fileSize: item.fileSize != null ? Number(item.fileSize) : null, + fileCount: item.fileNums != null ? Number(item.fileNums) : null, + repository: normalizeText(item.repository), + keywords: Array.isArray(item.keywords) ? item.keywords.join(', ') : '', + publisher: normalizeText(item.publisherName || item.authorName), + org: normalizeText(item.org), + dependencies: item.dependencies?.total != null ? Number(item.dependencies.total) : null, + devDependencies: item.devDependencies?.total != null ? Number(item.devDependencies.total) : null, + dependents: item.dependent?.total != null ? Number(item.dependent.total) : null, + versions: versionCount(item.versions), + published: dateFromMs(item.publishTime), + url: packageUrl(item.name), + }]; + }, +}); diff --git a/clis/ohpm/search.js b/clis/ohpm/search.js new file mode 100644 index 000000000..9c3f5297b --- /dev/null +++ b/clis/ohpm/search.js @@ -0,0 +1,71 @@ +// ohpm search โ€” search the OpenHarmony OHPM third-party package registry. +// +// Hits the public `oh-package/openapi/v1/search` endpoint used by +// https://ohpm.openharmony.cn/ and returns package rows that round-trip into +// `ohpm package`. +import { cli, Strategy } from '@jackwener/opencli/registry'; +import { EmptyResultError } from '@jackwener/opencli/errors'; +import { + OHPM_API, + dateFromMs, + normalizeText, + ohpmFetch, + packageUrl, + requireBoundedInt, + requireSort, + requireString, +} from './utils.js'; + +cli({ + site: 'ohpm', + name: 'search', + access: 'read', + description: 'Search OpenHarmony OHPM third-party packages by keyword', + domain: 'ohpm.openharmony.cn', + strategy: Strategy.PUBLIC, + browser: false, + args: [ + { name: 'query', positional: true, required: true, help: 'Package keyword (e.g. "axios", "json")' }, + { name: 'limit', type: 'int', default: 20, help: 'Max results (1-50)' }, + { name: 'sort', type: 'string', default: 'relevancy', help: 'Sort: relevancy, likes, latest' }, + ], + columns: [ + 'rank', 'name', 'latestVersion', 'description', 'license', 'keywords', + 'likes', 'points', 'popularity', 'publisher', 'org', 'published', 'url', + ], + func: async (args) => { + const query = requireString(args.query, 'query'); + const limit = requireBoundedInt(args.limit, 20, 50); + const sort = requireSort(args.sort); + const params = new URLSearchParams({ + condition: query, + pageNum: '1', + pageSize: String(limit), + sortedType: sort, + isHomePage: 'false', + }); + const body = await ohpmFetch(`${OHPM_API}/v1/search?${params}`, 'ohpm search'); + const rows = Array.isArray(body?.body?.rows) ? body.body.rows : []; + if (!rows.length) { + throw new EmptyResultError('ohpm search', `No OHPM packages matched "${query}".`); + } + return rows.slice(0, limit).map((item, i) => { + const name = normalizeText(item.name); + return { + rank: i + 1, + name, + latestVersion: normalizeText(item.latestVersion), + description: normalizeText(item.description), + license: normalizeText(item.license), + keywords: Array.isArray(item.keywords) ? item.keywords.join(', ') : '', + likes: item.likes != null ? Number(item.likes) : null, + points: item.points != null ? Number(item.points) : null, + popularity: item.popularity != null ? Number(item.popularity) : null, + publisher: normalizeText(item.publisherName || item.authorName), + org: normalizeText(item.org), + published: dateFromMs(item.latestPublishTime), + url: name ? packageUrl(name) : '', + }; + }); + }, +}); diff --git a/clis/ohpm/utils.js b/clis/ohpm/utils.js new file mode 100644 index 000000000..4a3d59646 --- /dev/null +++ b/clis/ohpm/utils.js @@ -0,0 +1,108 @@ +import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors'; + +export const OHPM_BASE = 'https://ohpm.openharmony.cn'; +export const OHPM_API = `${OHPM_BASE}/ohpmweb/registry/oh-package/openapi`; +const UA = 'opencli-ohpm-adapter (+https://github.com/jackwener/opencli)'; +const PACKAGE_NAME = /^(?:@[A-Za-z0-9][A-Za-z0-9._-]*\/)?[A-Za-z0-9][A-Za-z0-9._-]*$/; + +export function normalizeText(value) { + return String(value ?? '').replace(/\s+/g, ' ').trim(); +} + +export function dateFromMs(value) { + const ms = Number(value); + if (!Number.isFinite(ms) || ms <= 0) return ''; + const date = new Date(ms); + return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 10); +} + +export function requireString(value, label) { + const text = normalizeText(value); + if (!text) throw new ArgumentError(`ohpm ${label} cannot be empty`); + return text; +} + +export function requirePackageName(value) { + const name = normalizeText(value); + if (!name) throw new ArgumentError('ohpm package name is required (e.g. "@ohos/axios")'); + if (name.length > 214) { + throw new ArgumentError(`ohpm package name "${value}" is too long (max 214 chars)`); + } + if (!PACKAGE_NAME.test(name)) { + throw new ArgumentError( + `ohpm package name "${value}" is not a valid package name`, + 'Names are 1-214 chars of letters / digits / "-._" (scoped form: "@scope/name").', + ); + } + return name; +} + +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(`ohpm ${label} must be a positive integer`); + } + if (n > maxValue) { + throw new ArgumentError(`ohpm ${label} must be <= ${maxValue}`); + } + return n; +} + +export function requireSort(value) { + const sort = normalizeText(value || 'relevancy'); + const aliases = { + relevance: 'relevancy', + relevant: 'relevancy', + popular: 'likes', + popularity: 'likes', + like: 'likes', + newest: 'latest', + recent: 'latest', + }; + const normalized = aliases[sort] || sort; + if (!['relevancy', 'likes', 'latest'].includes(normalized)) { + throw new ArgumentError( + `ohpm sort must be one of relevancy, likes, latest; got "${value}"`, + 'The OHPM public API currently rejects other sort keys.', + ); + } + return normalized; +} + +export function packageUrl(name) { + return `${OHPM_BASE}/#/cn/detail/${encodeURIComponent(name)}`; +} + +export async function ohpmFetch(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 ohpm.openharmony.cn is reachable from this network.', + ); + } + if (resp.status === 404) { + throw new EmptyResultError(label, `OHPM returned 404 for ${url}.`); + } + if (resp.status === 429) { + throw new CommandExecutionError( + `${label} returned HTTP 429 (rate limited)`, + 'OHPM throttles bursts; wait a few seconds and retry.', + ); + } + let body; + try { + body = await resp.json(); + } + catch (err) { + throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`); + } + if (!resp.ok || body?.code && body.code !== 200) { + throw new CommandExecutionError(`${label} returned HTTP ${resp.status}: ${body?.message ?? body?.code ?? 'unknown error'}`); + } + return body; +} diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index ab6a8bc13..c0b2ed3ca 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -141,6 +141,7 @@ export default defineConfig({ { text: 'LessWrong', link: '/adapters/browser/lesswrong' }, { text: 'Lobsters', link: '/adapters/browser/lobsters' }, { text: 'Steam', link: '/adapters/browser/steam' }, + { text: 'OHPM', link: '/adapters/browser/ohpm' }, ], }, { diff --git a/docs/adapters/browser/ohpm.md b/docs/adapters/browser/ohpm.md new file mode 100644 index 000000000..0fb56fd1f --- /dev/null +++ b/docs/adapters/browser/ohpm.md @@ -0,0 +1,81 @@ +# OHPM + +**Mode**: ๐ŸŒ Public ยท **Domain**: `ohpm.openharmony.cn` + +Search and inspect OpenHarmony/HarmonyOS third-party packages from the public OHPM registry. The adapter uses the same unauthenticated JSON endpoints as `https://ohpm.openharmony.cn/`, so no browser session or login is required. + +## Commands + +| Command | Description | +|---------|-------------| +| `opencli ohpm search ` | Search OpenHarmony OHPM third-party packages by keyword | +| `opencli ohpm package ` | Single OHPM package metadata (version, downloads, license, repository) | +| `opencli ohpm dependents ` | List packages that depend on an OHPM package | +| `opencli ohpm keywords` | List hot OHPM search keywords | + +## Usage Examples + +```bash +# Search packages +opencli ohpm search axios --limit 10 +opencli ohpm search json --sort latest + +# Inspect a single package (use `name` from search rows) +opencli ohpm package @ohos/axios +opencli ohpm package @ohos/axios --version 2.2.10 + +# Reverse dependencies +opencli ohpm dependents @ohos/axios --limit 20 + +# Home-page hot keywords +opencli ohpm keywords + +# JSON output +opencli ohpm package @ohos/axios -f json +``` + +## Output Columns + +| Command | Columns | +|---------|---------| +| `search` | `rank, name, latestVersion, description, license, keywords, likes, points, popularity, publisher, org, published, url` | +| `package` | `name, version, description, license, downloads, likes, points, popularity, fileSize, fileCount, repository, keywords, publisher, org, dependencies, devDependencies, dependents, versions, published, url` | +| `dependents` | `rank, name, version, dependent, url` | +| `keywords` | `rank, keyword` | + +The `name` column from `search` round-trips into `package` and `dependents`. + +## Options + +### `search` + +| Option | Description | +|--------|-------------| +| `query` (positional) | Free-text search query | +| `--limit` | Max results (1-50, default: 20) | +| `--sort` | One of `relevancy`, `likes`, `latest` (default: `relevancy`). Aliases: `popular` โ†’ `likes`, `newest` โ†’ `latest`. | + +### `package` + +| Option | Description | +|--------|-------------| +| `name` (positional) | OHPM package name (e.g. `@ohos/axios`) | +| `--version` | Package version; omit for latest | + +### `dependents` + +| Option | Description | +|--------|-------------| +| `name` (positional) | OHPM package name | +| `--version` | Package version; omit for latest | +| `--limit` | Max dependents (1-50, default: 20) | + +## Caveats + +- The OHPM public API currently accepts `sortedType` values `relevancy`, `likes`, and `latest`. Unsupported values such as `download` are rejected by the server and surfaced as typed command errors. +- Some package detail responses omit `description`; latest-package lookups fill it from the exact package row in search metadata when available. +- `dependents` returns the first page exposed by the package detail endpoint. + +## Prerequisites + +- No browser required โ€” uses public OHPM registry endpoints. diff --git a/docs/adapters/index.md b/docs/adapters/index.md index 948470f72..00224d76a 100644 --- a/docs/adapters/index.md +++ b/docs/adapters/index.md @@ -119,6 +119,7 @@ Run `opencli list` for the live registry. | **[steam](./browser/steam.md)** | `top-sellers` | ๐ŸŒ Public | | **[coingecko](./browser/coingecko.md)** | `top` `coin` `trending` `exchanges` `categories` `derivatives` `global` | ๐ŸŒ Public | | **[npm](./browser/npm.md)** | `search` `package` `downloads` | ๐ŸŒ Public | +| **[ohpm](./browser/ohpm.md)** | `search` `package` `dependents` `keywords` | ๐ŸŒ Public | | **[pypi](./browser/pypi.md)** | `package` `downloads` | ๐ŸŒ Public | | **[crates](./browser/crates.md)** | `search` `crate` | ๐ŸŒ Public | | **[mdn](./browser/mdn.md)** | `search` | ๐ŸŒ Public |