9b395f5cc3
Build Chrome Extension / build (push) Waiting to run
Trigger Website Rebuild (Docs Updated) / dispatch (push) Waiting to run
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
115 lines
4.6 KiB
JavaScript
115 lines
4.6 KiB
JavaScript
// Shared helpers for the Flathub adapters (https://flathub.org).
|
|
//
|
|
// Flathub is the canonical Linux flatpak app registry. Public REST API at
|
|
// `flathub.org/api/v2`, no auth, no key. Two endpoints we surface:
|
|
// • POST /search → keyword search, returns app metadata
|
|
// • GET /appstream/<appId> → full appstream metadata for one app
|
|
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
|
|
export const FLATHUB_API_BASE = 'https://flathub.org/api/v2';
|
|
export const FLATHUB_APP_BASE = 'https://flathub.org/apps';
|
|
const UA = 'opencli-flathub-adapter/1.0 (+https://github.com/jackwener/opencli; mailto:opencli@example.com)';
|
|
|
|
// AppStream IDs are reverse-DNS (e.g. "org.gnome.Calculator"); the spec allows
|
|
// letters, digits, `.`, `_`, `-`. Min two segments separated by `.`.
|
|
const APP_ID_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_][A-Za-z0-9_-]*){1,}$/;
|
|
|
|
export function requireString(value, label) {
|
|
const s = String(value ?? '').trim();
|
|
if (!s) throw new ArgumentError(`flathub ${label} cannot be empty`);
|
|
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(`flathub ${label} must be a positive integer`);
|
|
}
|
|
if (n > maxValue) {
|
|
throw new ArgumentError(`flathub ${label} must be <= ${maxValue}`);
|
|
}
|
|
return n;
|
|
}
|
|
|
|
export function requireAppId(value) {
|
|
const raw = String(value ?? '').trim();
|
|
if (!raw) throw new ArgumentError('flathub appId is required (e.g. "org.mozilla.firefox")');
|
|
if (!APP_ID_PATTERN.test(raw)) {
|
|
throw new ArgumentError(
|
|
`flathub appId "${value}" is not a valid AppStream identifier`,
|
|
'AppStream IDs use reverse-DNS like "org.mozilla.firefox" — letters/digits/`._-` with at least one dot.',
|
|
);
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
export async function flathubFetch(url, label, init) {
|
|
let resp;
|
|
try {
|
|
resp = await fetch(url, {
|
|
method: init?.method ?? 'GET',
|
|
headers: { 'user-agent': UA, accept: 'application/json', ...(init?.headers ?? {}) },
|
|
body: init?.body,
|
|
});
|
|
}
|
|
catch (err) {
|
|
throw new CommandExecutionError(
|
|
`${label} request failed: ${err?.message ?? err}`,
|
|
'Check that flathub.org is reachable from this network.',
|
|
);
|
|
}
|
|
if (resp.status === 404) {
|
|
throw new EmptyResultError(label, `Flathub returned 404 for ${url}.`);
|
|
}
|
|
if (resp.status === 429) {
|
|
throw new CommandExecutionError(`${label} returned HTTP 429 (rate limited)`);
|
|
}
|
|
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;
|
|
}
|
|
|
|
export function joinList(value, max = 10) {
|
|
if (!Array.isArray(value)) return '';
|
|
const items = value.filter((v) => typeof v === 'string' && v.trim());
|
|
if (items.length === 0) return '';
|
|
if (items.length > max) return [...items.slice(0, max), `(+${items.length - max})`].join(', ');
|
|
return items.join(', ');
|
|
}
|
|
|
|
// Coerce flathub's `timestamp` field (sometimes int, sometimes numeric string).
|
|
function timestampNumber(value) {
|
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
if (typeof value === 'string' && /^\d+$/.test(value.trim())) return Number(value.trim());
|
|
return 0;
|
|
}
|
|
|
|
/** Pick the most recent appstream `releases[].version` if present. */
|
|
export function pickLatestRelease(releases) {
|
|
if (!Array.isArray(releases) || releases.length === 0) return { version: null, date: null };
|
|
// releases[].timestamp is unix-seconds; flathub returns either int or numeric string.
|
|
const sorted = [...releases].filter((r) => r && typeof r === 'object').sort((a, b) => {
|
|
return timestampNumber(b?.timestamp) - timestampNumber(a?.timestamp);
|
|
});
|
|
const top = sorted[0];
|
|
if (!top) return { version: null, date: null };
|
|
let date = null;
|
|
const ts = timestampNumber(top.timestamp);
|
|
if (ts > 0) {
|
|
const d = new Date(ts * 1000);
|
|
if (!Number.isNaN(d.getTime())) date = d.toISOString().slice(0, 10);
|
|
}
|
|
// Fallback: appstream emits `date` as ISO string ("2024-12-09") for some apps.
|
|
if (!date && typeof top.date === 'string' && top.date.trim()) date = top.date.trim();
|
|
return { version: typeof top.version === 'string' ? top.version : null, date };
|
|
}
|