- Create github-skill-helpers.ts with fetchText, fetchJson, resolveGitHubDefaultBranch, resolveGitHubCommitSha, parseGitHubSourceUrl, resolveGitHubPinnedRef, resolveRawGitHubUrl - Update company-skills.ts to import from github-skill-helpers.js instead of defining locally - All existing company-skill tests pass (15/15)
102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
import path from "node:path";
|
|
import { unprocessable } from "../errors.js";
|
|
|
|
function asString(value: unknown): string | undefined {
|
|
if (typeof value !== "string") return undefined;
|
|
const trimmed = value.trim();
|
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
}
|
|
|
|
export async function fetchText(url: string): Promise<string> {
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw unprocessable(`Failed to fetch ${url}: ${response.status}`);
|
|
}
|
|
return response.text();
|
|
}
|
|
|
|
export async function fetchJson<T>(url: string): Promise<T> {
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
accept: "application/vnd.github+json",
|
|
},
|
|
});
|
|
if (!response.ok) {
|
|
throw unprocessable(`Failed to fetch ${url}: ${response.status}`);
|
|
}
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
export async function resolveGitHubDefaultBranch(owner: string, repo: string): Promise<string> {
|
|
const response = await fetchJson<{ default_branch?: string }>(
|
|
`https://api.github.com/repos/${owner}/${repo}`,
|
|
);
|
|
return asString(response.default_branch) ?? "main";
|
|
}
|
|
|
|
export async function resolveGitHubCommitSha(owner: string, repo: string, ref: string): Promise<string> {
|
|
const response = await fetchJson<{ sha?: string }>(
|
|
`https://api.github.com/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`,
|
|
);
|
|
const sha = asString(response.sha);
|
|
if (!sha) {
|
|
throw unprocessable(`Failed to resolve GitHub ref ${ref}`);
|
|
}
|
|
return sha;
|
|
}
|
|
|
|
export function parseGitHubSourceUrl(rawUrl: string): {
|
|
owner: string;
|
|
repo: string;
|
|
ref: string;
|
|
basePath: string;
|
|
filePath: string | null;
|
|
explicitRef: boolean;
|
|
} {
|
|
const url = new URL(rawUrl);
|
|
if (url.hostname !== "github.com") {
|
|
throw unprocessable("GitHub source must use github.com URL");
|
|
}
|
|
const parts = url.pathname.split("/").filter(Boolean);
|
|
if (parts.length < 2) {
|
|
throw unprocessable("Invalid GitHub URL");
|
|
}
|
|
const owner = parts[0]!;
|
|
const repo = parts[1]!.replace(/\.git$/i, "");
|
|
let ref = "main";
|
|
let basePath = "";
|
|
let filePath: string | null = null;
|
|
let explicitRef = false;
|
|
if (parts[2] === "tree") {
|
|
ref = parts[3] ?? "main";
|
|
basePath = parts.slice(4).join("/");
|
|
explicitRef = true;
|
|
} else if (parts[2] === "blob") {
|
|
ref = parts[3] ?? "main";
|
|
filePath = parts.slice(4).join("/");
|
|
basePath = filePath ? path.posix.dirname(filePath) : "";
|
|
explicitRef = true;
|
|
}
|
|
return { owner, repo, ref, basePath, filePath, explicitRef };
|
|
}
|
|
|
|
export async function resolveGitHubPinnedRef(
|
|
parsed: ReturnType<typeof parseGitHubSourceUrl>,
|
|
): Promise<{ pinnedRef: string; trackingRef: string | null }> {
|
|
if (/^[0-9a-f]{40}$/i.test(parsed.ref.trim())) {
|
|
return {
|
|
pinnedRef: parsed.ref,
|
|
trackingRef: parsed.explicitRef ? parsed.ref : null,
|
|
};
|
|
}
|
|
|
|
const trackingRef = parsed.explicitRef
|
|
? parsed.ref
|
|
: await resolveGitHubDefaultBranch(parsed.owner, parsed.repo);
|
|
const pinnedRef = await resolveGitHubCommitSha(parsed.owner, parsed.repo, trackingRef);
|
|
return { pinnedRef, trackingRef };
|
|
}
|
|
|
|
export function resolveRawGitHubUrl(owner: string, repo: string, ref: string, filePath: string): string {
|
|
return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filePath.replace(/^\/+/, "")}`;
|
|
}
|