feat(09-01): extract GitHub fetch helpers to shared module

- 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)
This commit is contained in:
Mikkel Georgsen 2026-04-01 01:01:37 +02:00
parent 16ceef77d2
commit d26b888957
2 changed files with 112 additions and 83 deletions

View file

@ -30,6 +30,15 @@ import { normalizeAgentUrlKey } from "@paperclipai/shared";
import { findServerAdapter } from "../adapters/index.js";
import { resolvePaperclipInstanceRoot } from "../home-paths.js";
import { notFound, unprocessable } from "../errors.js";
import {
fetchText,
fetchJson,
resolveGitHubDefaultBranch,
resolveGitHubCommitSha,
parseGitHubSourceUrl,
resolveGitHubPinnedRef,
resolveRawGitHubUrl,
} from "./github-skill-helpers.js";
import { agentService } from "./agents.js";
import { projectService } from "./projects.js";
import { secretService } from "./secrets.js";
@ -469,90 +478,8 @@ function parseFrontmatterMarkdown(raw: string): { frontmatter: Record<string, un
};
}
async function fetchText(url: string) {
const response = await fetch(url);
if (!response.ok) {
throw unprocessable(`Failed to fetch ${url}: ${response.status}`);
}
return response.text();
}
// [nexus] GitHub helpers extracted to shared module — imported below
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>;
}
async function resolveGitHubDefaultBranch(owner: string, repo: string) {
const response = await fetchJson<{ default_branch?: string }>(
`https://api.github.com/repos/${owner}/${repo}`,
);
return asString(response.default_branch) ?? "main";
}
async function resolveGitHubCommitSha(owner: string, repo: string, ref: 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;
}
function parseGitHubSourceUrl(rawUrl: string) {
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 };
}
async function resolveGitHubPinnedRef(parsed: ReturnType<typeof parseGitHubSourceUrl>) {
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 };
}
function resolveRawGitHubUrl(owner: string, repo: string, ref: string, filePath: string) {
return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filePath.replace(/^\/+/, "")}`;
}
function extractCommandTokens(raw: string) {
const matches = raw.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];

View file

@ -0,0 +1,102 @@
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(/^\/+/, "")}`;
}