// The API this extension exports to others (spec §9, "extension API"): the // same idea as the built-in Git extension's `getAPI()`. A forge extension // such as oxygenhub-vscode consumes it instead of running oxy itself, and // registers a remote provider so this extension can offer "Open on …" // links without knowing any hub. // // Pure functions live here so they can be tested with plain Node; the // vscode-facing object is assembled in extension.ts. /** What a remote-aware extension registers. */ export interface RemoteProvider { /** A stable id, e.g. `oxygenhub`. */ readonly id: string; /** Shown in menus: "Open on OxygenHub". */ readonly label: string; /** Whether this provider understands a remote URL. */ matches(remoteUrl: string): boolean; /** A web URL for a thing in that remote, or undefined when the provider has none. */ urlFor(remoteUrl: string, target: RemoteTarget): string | undefined; } export type RemoteTarget = | { kind: "repository" } | { kind: "bookmark"; name: string } | { kind: "change"; hash: string } | { kind: "file"; reference: string; path: string; line?: number } | { kind: "proposals" } | { kind: "issues" }; /** A minimal view of one repository, stable across versions. */ export interface RepositoryHandle { readonly root: string; /** `oxy status --json` as last refreshed, or undefined before the first refresh. */ readonly status: unknown; readonly bookmarks: { name: string; heads: string[]; current?: boolean }[]; readonly currentBookmark: string | undefined; remotes(): Promise<{ name: string; url: string }[]>; /** Run an oxy command in this repository and parse its JSON output. */ json(args: string[]): Promise; refresh(): Promise; } export interface OxygenAPI { readonly version: 1; readonly repositories: readonly RepositoryHandle[]; /** Fires with the repository whose status changed. */ readonly onDidChangeStatus: (listener: (repo: RepositoryHandle) => void) => { dispose(): void }; /** Fires when repositories are discovered or removed. */ readonly onDidChangeRepositories: (listener: () => void) => { dispose(): void }; getRepository(fsPath: string): RepositoryHandle | undefined; registerRemoteProvider(provider: RemoteProvider): { dispose(): void }; /** The providers registered so far, for callers that build their own menus. */ remoteProviders(): readonly RemoteProvider[]; } /** The provider (and remote) to use for a repository's remotes, if any. */ export function pickProvider(providers: readonly RemoteProvider[], remotes: { name: string; url: string }[]): { provider: RemoteProvider; remote: { name: string; url: string } } | undefined { // Prefer a remote called origin or earth, then any. const ordered = [...remotes].sort((a, b) => rank(a.name) - rank(b.name)); for (const remote of ordered) { for (const provider of providers) { if (provider.matches(remote.url)) { return { provider, remote }; } } } return undefined; } function rank(name: string): number { return name === "origin" ? 0 : name === "hub" ? 1 : 2; } /** The repository handle a folder path belongs to: the longest root that prefixes it. */ export function repositoryFor(roots: string[], fsPath: string): string | undefined { let best: string | undefined; for (const r of roots) { if ((fsPath === r || fsPath.startsWith(r.endsWith("/") ? r : r + "/")) && (!best || r.length > best.length)) { best = r; } } return best; }