Add marketplace feed refresh command (#96155)

Merged via squash.

Prepared head SHA: 1614070e92
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Co-authored-by: giodl73-repo <235387111+giodl73-repo@users.noreply.github.com>
Reviewed-by: @giodl73-repo
This commit is contained in:
Gio Della-Libera
2026-06-28 10:29:15 -07:00
committed by GitHub
parent 891096926e
commit 102a65477e
7 changed files with 743 additions and 10 deletions

View File

@@ -17,7 +17,11 @@ import { tracePluginLifecyclePhaseAsync } from "../plugins/plugin-lifecycle-trac
import { defaultRuntime } from "../runtime.js";
import { shortenHomeInString } from "../utils.js";
import { formatMissingPluginMessage } from "./error-format.js";
import type { PluginMarketplaceListOptions, PluginRegistryOptions } from "./plugins-cli.js";
import type {
PluginMarketplaceListOptions,
PluginMarketplaceRefreshOptions,
PluginRegistryOptions,
} from "./plugins-cli.js";
type PluginInstallActionOptions = {
acknowledgeClawHubRisk?: boolean;
@@ -443,6 +447,200 @@ export async function runPluginsDoctorCommand(): Promise<void> {
defaultRuntime.log(lines.join("\n"));
}
type MarketplaceRefreshPayload = {
source: "hosted" | "hosted-snapshot" | "bundled-fallback";
entries: number;
feed?: {
id: string;
generatedAt: string;
sequence: number;
};
metadata?: {
url: string;
status: number;
etag?: string;
lastModified?: string;
checksum?: string;
};
snapshot?: {
savedAt: string;
};
error?: string;
};
function buildMarketplaceRefreshPayload(
result: Awaited<
ReturnType<
typeof import("../plugins/official-external-plugin-catalog.js").loadConfiguredHostedOfficialExternalPluginCatalogEntries
>
>,
): MarketplaceRefreshPayload {
const payload: MarketplaceRefreshPayload = {
source: result.source,
entries: result.entries.length,
...(result.metadata ? { metadata: result.metadata } : {}),
};
if (result.source === "hosted" || result.source === "hosted-snapshot") {
payload.feed = {
id: result.feed.id,
generatedAt: result.feed.generatedAt,
sequence: result.feed.sequence,
};
}
if (result.source === "hosted-snapshot") {
payload.snapshot = { savedAt: result.snapshot.savedAt };
payload.error = result.error;
}
if (result.source === "bundled-fallback") {
payload.error = result.error;
}
return payload;
}
function redactMarketplaceFeedUrl(value: string): string {
try {
const url = new URL(value);
url.username = "";
url.password = "";
url.search = "";
url.hash = "";
return url.href;
} catch {
return value;
}
}
function replaceAllLiteral(value: string, search: string, replacement: string): string {
return search ? value.split(search).join(replacement) : value;
}
function redactMarketplaceOutputText(
value: string,
rawUrls: readonly (string | undefined)[],
): string {
let redacted = value;
for (const rawUrl of rawUrls) {
if (!rawUrl) {
continue;
}
redacted = replaceAllLiteral(redacted, rawUrl, redactMarketplaceFeedUrl(rawUrl));
}
return redacted;
}
function sanitizeMarketplaceRefreshPayload(
payload: MarketplaceRefreshPayload,
params?: { feedUrl?: string },
): MarketplaceRefreshPayload {
const rawMetadataUrl = payload.metadata?.url;
const sanitized: MarketplaceRefreshPayload = {
...payload,
...(payload.metadata
? { metadata: { ...payload.metadata, url: redactMarketplaceFeedUrl(payload.metadata.url) } }
: {}),
};
if (payload.error) {
sanitized.error = redactMarketplaceOutputText(payload.error, [params?.feedUrl, rawMetadataUrl]);
}
return sanitized;
}
function formatMarketplaceRefreshSource(source: MarketplaceRefreshPayload["source"]): string {
if (source === "hosted") {
return theme.success("hosted");
}
if (source === "hosted-snapshot") {
return theme.warn("hosted snapshot");
}
return theme.warn("bundled fallback");
}
function shouldFailPinnedMarketplaceRefresh(params: {
expectedSha256?: string;
source: MarketplaceRefreshPayload["source"];
}): boolean {
return Boolean(params.expectedSha256?.trim()) && params.source !== "hosted";
}
function normalizeMarketplaceExpectedSha256(value: string | undefined): string | undefined {
const trimmed = value?.trim();
if (!trimmed) {
return undefined;
}
if (/^[0-9a-f]{64}$/iu.test(trimmed)) {
return `sha256:${trimmed.toLowerCase()}`;
}
const prefixed = /^sha256:([0-9a-f]{64})$/iu.exec(trimmed);
if (prefixed?.[1]) {
return `sha256:${prefixed[1].toLowerCase()}`;
}
return trimmed;
}
function formatPinnedMarketplaceRefreshFailure(payload: MarketplaceRefreshPayload): string {
return `Pinned marketplace feed refresh did not accept a fresh hosted payload (source: ${payload.source}).`;
}
/** Refresh the configured OpenClaw marketplace feed snapshot. */
export async function runPluginMarketplaceRefreshCommand(
opts: PluginMarketplaceRefreshOptions,
): Promise<void> {
const { loadConfiguredHostedOfficialExternalPluginCatalogEntries } =
await import("../plugins/official-external-plugin-catalog.js");
const cfg = getRuntimeConfig();
const expectedSha256 = normalizeMarketplaceExpectedSha256(opts.expectedSha256);
const result = await loadConfiguredHostedOfficialExternalPluginCatalogEntries(cfg, {
...(opts.feedProfile ? { feedProfile: opts.feedProfile } : {}),
...(opts.feedUrl ? { feedUrl: opts.feedUrl } : {}),
...(expectedSha256 ? { expectedSha256 } : {}),
requireSnapshotWrite: true,
});
const payload = sanitizeMarketplaceRefreshPayload(buildMarketplaceRefreshPayload(result), {
feedUrl: opts.feedUrl,
});
const failedPinnedRefresh = shouldFailPinnedMarketplaceRefresh({
expectedSha256,
source: payload.source,
});
if (opts.json) {
defaultRuntime.writeJson(payload);
if (failedPinnedRefresh) {
defaultRuntime.error(formatPinnedMarketplaceRefreshFailure(payload));
return defaultRuntime.exit(1);
}
return;
}
const lines = [
`${theme.muted("Source:")} ${formatMarketplaceRefreshSource(payload.source)}`,
`${theme.muted("Entries:")} ${payload.entries}`,
];
if (payload.feed) {
lines.push(
`${theme.muted("Feed:")} ${payload.feed.id} ${theme.muted(`sequence ${payload.feed.sequence}`)}`,
);
}
if (payload.metadata?.url) {
lines.push(`${theme.muted("URL:")} ${payload.metadata.url}`);
}
if (payload.metadata?.checksum) {
lines.push(`${theme.muted("SHA-256:")} ${payload.metadata.checksum}`);
}
if (payload.snapshot?.savedAt) {
lines.push(`${theme.muted("Snapshot:")} ${payload.snapshot.savedAt}`);
}
if (payload.error) {
lines.push(`${theme.muted("Fallback reason:")} ${payload.error}`);
}
defaultRuntime.log(lines.join("\n"));
if (failedPinnedRefresh) {
defaultRuntime.error(formatPinnedMarketplaceRefreshFailure(payload));
return defaultRuntime.exit(1);
}
}
/** List plugins from a configured marketplace manifest. */
export async function runPluginMarketplaceListCommand(
source: string,