Files
orca/src/main/plugins/plugin-cli-catalogs.ts
T
Neil be3b87f3ac cli: add orca plugin install, update, uninstall and catalog list/add/rm (#36647)
* cli: add orca plugin install, update, uninstall and catalog list/add/rm

* plugins: escape the invisible-character regex so the orcad bundle stays ASCII

* plugins: split approval re-run helpers and plugin RPC binding to stay under max-lines
2026-11-06 14:20:27 -08:00

116 lines
4.5 KiB
TypeScript

import {
buildPluginCatalogApprovalRequest,
PluginCatalogApprovalRequiredError
} from '../../shared/plugins/approval/plugin-catalog-cli-approval'
import {
PLUGIN_NOT_FOUND_CODE,
PluginCliError
} from '../../shared/plugins/approval/plugin-cli-approval'
import { isSameGitRepository } from '../../shared/plugins/catalog/plugin-marketplace'
import { parsePluginGitInstallInput } from '../../shared/plugins/installation/plugin-git-install-input'
import type { PluginMarketplaceSourceState } from './catalog/plugin-marketplace-service'
import { matchesCatalogSelector, type PluginCliCatalogs } from './plugin-cli-catalog-candidates'
import { gitFailure, type PluginCliCandidateHost } from './plugin-cli-install-candidates'
import { assertPluginSystemEnabled } from './plugin-dev-folders'
export type PluginCliCatalogEntry = {
id: string
url: string
ref: string
name: string | null
owner: string | null
official: boolean
fetchedAt: number | null
stale: boolean
error: string | null
}
type CatalogDeps = {
catalogs: PluginCliCatalogs
host: Pick<PluginCliCandidateHost, 'executionHost'>
service: Parameters<typeof assertPluginSystemEnabled>[0]
}
export function pluginCliCatalogEntry(state: PluginMarketplaceSourceState): PluginCliCatalogEntry {
return {
id: state.id,
url: state.source.url,
ref: state.source.ref,
name: state.marketplace?.name ?? null,
owner: state.marketplace?.owner ?? null,
official: state.official,
fetchedAt: state.marketplace?.fetchedAt ?? null,
stale: state.stale,
error: state.error ?? null
}
}
export async function listPluginCatalogsFromCli(
deps: CatalogDeps
): Promise<PluginCliCatalogEntry[]> {
assertPluginSystemEnabled(deps.service)
return (await deps.catalogs.marketplace.listSources()).map(pluginCliCatalogEntry)
}
/** Adds a catalog only with `--approve` for its exact URL and branch; re-adding just refreshes it. */
export async function addPluginCatalogFromCli(
deps: CatalogDeps,
input: { input: string; ref?: string; approvalFingerprint?: string }
): Promise<{ added: boolean; catalog: PluginCliCatalogEntry }> {
assertPluginSystemEnabled(deps.service)
const parsed = parsePluginGitInstallInput(input.input)
if (!parsed.ok) {
throw new PluginCliError(
'invalid_argument',
parsed.reason === 'git-url-credentials'
? "Remove the username or token from the URL. Orca uses the Orca host's own Git sign-in."
: 'Pass the catalog repository as owner/repo, an https:// or ssh:// URL, or git@host:owner/repo.git.',
{ nextSteps: ['Fix the URL and run the command again.'] }
)
}
const source = { kind: 'git' as const, url: parsed.url, ref: input.ref ?? (parsed.ref || 'main') }
const existing = (await deps.catalogs.marketplace.listSources()).find(
(state) => isSameGitRepository(state.source.url, source.url) && state.source.ref === source.ref
)
if (existing) {
const refreshed = await deps.catalogs.marketplace.refreshSource(existing.id)
return { added: false, catalog: pluginCliCatalogEntry(refreshed) }
}
const request = buildPluginCatalogApprovalRequest(source)
if (input.approvalFingerprint !== request.approvalFingerprint) {
throw new PluginCatalogApprovalRequiredError(request, input.approvalFingerprint !== undefined)
}
const added = await deps.catalogs.marketplace.addSource(source).catch((error: unknown) => {
throw gitFailure(deps.host, source.url, error)
})
return { added: true, catalog: pluginCliCatalogEntry(added) }
}
/** Removing a catalog only takes trust away, so it needs no approval. Orca's own cannot be removed. */
export async function removePluginCatalogFromCli(
deps: CatalogDeps,
selector: string
): Promise<{ removed: PluginCliCatalogEntry }> {
assertPluginSystemEnabled(deps.service)
const matches = (await deps.catalogs.marketplace.listSources()).filter((state) =>
matchesCatalogSelector(state, selector)
)
if (matches.length !== 1) {
throw new PluginCliError(
matches.length === 0 ? PLUGIN_NOT_FOUND_CODE : 'invalid_argument',
matches.length === 0
? `No catalog on this Orca host matches ${selector}.`
: `${selector} matches more than one catalog; pass its id.`,
{ nextSteps: ['`orca plugin catalog list` shows each catalog id.'] }
)
}
const [state] = matches
if (state.official) {
throw new PluginCliError('invalid_argument', "Orca's official catalog cannot be removed.", {
nextSteps: []
})
}
await deps.catalogs.marketplace.removeSource(state.id)
return { removed: pluginCliCatalogEntry(state) }
}