Files
paperclip/server/src/services/gitea-fetch.ts
T
Chris Farhood e559218f98 fork: add Gitea/Forgejo source support for company skills
Reintroduce Gitea/Forgejo as a skill import source on dev only, since
the fork deploys against git.farh.net. Pasting a Gitea/Forgejo repo
URL into the skills sidebar mirrors the existing GitHub experience:
pin to a commit SHA, check for updates, read repo files.

Server: new gitea-fetch.ts (URL builders, probe-cache helpers) and
gitea-skills.ts (parse, probe, pin, tree, text, branch). Dispatch in
readUrlSkillImports probes /api/v1/version and routes non-github.com
hosts into the new readGiteaUrlSkillImports branch. updateStatus and
readFile get a gitea arm alongside the github/skills_sh arm. Audit
falls through to "remote not supported" the same way github does.

UI: Server icon, Gitea source label, gitea in the "external" source
class, Pin/Update UI gate widened to sourceType === "gitea". CLI help
text updated. Existing github code is left byte-for-byte unchanged
(wrapped in isGitHubDotCom) so dev <-> master syncs stay clean.

PAT support, gitea portability descriptors, and gitea audit are
deliberate follow-ups. Detection requires /api/v1/version to return
Gitea-shaped JSON; the per-host result is cached for process lifetime
with FIFO eviction at 1024 entries. Non-Gitea hosts fall through to
the existing raw-markdown url branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 15:30:44 -04:00

76 lines
2.2 KiB
TypeScript

import { unprocessable } from "../errors.js";
const PROBE_CACHE_MAX_ENTRIES = 1024;
/**
* Process-lifetime cache of Gitea/Forgejo probe results.
* Keyed by lowercased hostname. Positive and negative results are both cached
* to avoid re-probing the same host on every import. FIFO-evicted at
* PROBE_CACHE_MAX_ENTRIES to bound memory.
*/
export const giteaHostProbeCache = new Map<string, boolean>();
function evictProbeCacheIfFull() {
if (giteaHostProbeCache.size > PROBE_CACHE_MAX_ENTRIES) {
const oldestKey = giteaHostProbeCache.keys().next().value;
if (oldestKey !== undefined) giteaHostProbeCache.delete(oldestKey);
}
}
export function setGiteaHostProbe(hostname: string, isGitea: boolean) {
giteaHostProbeCache.set(hostname.toLowerCase(), isGitea);
evictProbeCacheIfFull();
}
export function getGiteaHostProbe(hostname: string): boolean | undefined {
return giteaHostProbeCache.get(hostname.toLowerCase());
}
/**
* Gitea/Forgejo API base. There is no dotcom short-circuit — every host uses
* the same /api/v1 path, including the public gitea.com instance.
*/
export function giteaApiBase(hostname: string) {
return `https://${hostname}/api/v1`;
}
/**
* Canonical raw-content URL for Gitea/Forgejo ≥ 1.18.
* Modern format: `https://{host}/{owner}/{repo}/raw/branch/{ref}/{path}`.
*/
export function resolveRawGiteaUrl(
hostname: string,
owner: string,
repo: string,
ref: string,
filePath: string,
) {
const p = filePath.replace(/^\/+/, "");
return `https://${hostname}/${owner}/${repo}/raw/branch/${ref}/${p}`;
}
/**
* Legacy raw-content URL for Gitea < 1.18 and some Forgejo setups.
* Format: `https://{host}/{owner}/{repo}/raw/{ref}/{path}`.
*/
export function resolveRawGiteaUrlLegacy(
hostname: string,
owner: string,
repo: string,
ref: string,
filePath: string,
) {
const p = filePath.replace(/^\/+/, "");
return `https://${hostname}/${owner}/${repo}/raw/${ref}/${p}`;
}
export async function giteaFetch(url: string, init?: RequestInit): Promise<Response> {
try {
return await fetch(url, init);
} catch {
throw unprocessable(
`Could not connect to ${new URL(url).hostname} — ensure the URL points to a Gitea/Forgejo instance`,
);
}
}