Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions cli-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
51 changes: 51 additions & 0 deletions clis/ohpm/dependents.js
Original file line number Diff line number Diff line change
@@ -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),
}));
},
});
27 changes: 27 additions & 0 deletions clis/ohpm/keywords.js
Original file line number Diff line number Diff line change
@@ -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),
}));
},
});
151 changes: 151 additions & 0 deletions clis/ohpm/ohpm.test.js
Original file line number Diff line number Diff line change
@@ -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' },
]);
});
});
Loading