feat: support GitHub Enterprise URLs for skill and company imports
This commit is contained in:
parent
6c2c63e0f1
commit
9e1ee925cd
6 changed files with 116 additions and 54 deletions
|
|
@ -766,7 +766,14 @@ export function isHttpUrl(input: string): boolean {
|
|||
}
|
||||
|
||||
export function isGithubUrl(input: string): boolean {
|
||||
return /^https?:\/\/github\.com\//i.test(input.trim());
|
||||
try {
|
||||
const url = new URL(input.trim());
|
||||
if (url.protocol !== "https:" && url.protocol !== "http:") return false;
|
||||
const segments = url.pathname.split("/").filter(Boolean);
|
||||
return segments.length >= 2;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isGithubSegment(input: string): boolean {
|
||||
|
|
@ -797,13 +804,15 @@ function normalizeGithubImportPath(input: string | null | undefined): string | n
|
|||
}
|
||||
|
||||
function buildGithubImportUrl(input: {
|
||||
hostname?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
ref?: string | null;
|
||||
path?: string | null;
|
||||
companyPath?: string | null;
|
||||
}): string {
|
||||
const url = new URL(`https://github.com/${input.owner}/${input.repo.replace(/\.git$/i, "")}`);
|
||||
const host = input.hostname || "github.com";
|
||||
const url = new URL(`https://${host}/${input.owner}/${input.repo.replace(/\.git$/i, "")}`);
|
||||
const ref = input.ref?.trim();
|
||||
if (ref) {
|
||||
url.searchParams.set("ref", ref);
|
||||
|
|
@ -835,13 +844,14 @@ export function normalizeGithubImportSource(input: string, refOverride?: string)
|
|||
}
|
||||
|
||||
if (!isGithubUrl(trimmed)) {
|
||||
throw new Error("GitHub source must be a github.com URL or owner/repo[/path] shorthand.");
|
||||
throw new Error("GitHub source must be a GitHub or GitHub Enterprise URL, or owner/repo[/path] shorthand.");
|
||||
}
|
||||
if (!ref) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
const url = new URL(trimmed);
|
||||
const hostname = url.hostname;
|
||||
const parts = url.pathname.split("/").filter(Boolean);
|
||||
if (parts.length < 2) {
|
||||
throw new Error("Invalid GitHub URL.");
|
||||
|
|
@ -852,18 +862,18 @@ export function normalizeGithubImportSource(input: string, refOverride?: string)
|
|||
const existingPath = normalizeGithubImportPath(url.searchParams.get("path"));
|
||||
const existingCompanyPath = normalizeGithubImportPath(url.searchParams.get("companyPath"));
|
||||
if (existingCompanyPath) {
|
||||
return buildGithubImportUrl({ owner, repo, ref, companyPath: existingCompanyPath });
|
||||
return buildGithubImportUrl({ hostname, owner, repo, ref, companyPath: existingCompanyPath });
|
||||
}
|
||||
if (existingPath) {
|
||||
return buildGithubImportUrl({ owner, repo, ref, path: existingPath });
|
||||
return buildGithubImportUrl({ hostname, owner, repo, ref, path: existingPath });
|
||||
}
|
||||
if (parts[2] === "tree") {
|
||||
return buildGithubImportUrl({ owner, repo, ref, path: parts.slice(4).join("/") });
|
||||
return buildGithubImportUrl({ hostname, owner, repo, ref, path: parts.slice(4).join("/") });
|
||||
}
|
||||
if (parts[2] === "blob") {
|
||||
return buildGithubImportUrl({ owner, repo, ref, companyPath: parts.slice(4).join("/") });
|
||||
return buildGithubImportUrl({ hostname, owner, repo, ref, companyPath: parts.slice(4).join("/") });
|
||||
}
|
||||
return buildGithubImportUrl({ owner, repo, ref });
|
||||
return buildGithubImportUrl({ hostname, owner, repo, ref });
|
||||
}
|
||||
|
||||
async function pathExists(inputPath: string): Promise<boolean> {
|
||||
|
|
@ -1214,7 +1224,7 @@ export function registerCompanyCommands(program: Command): void {
|
|||
if (!isGithubUrl(from) && !isGithubShorthand(from)) {
|
||||
throw new Error(
|
||||
"Only GitHub URLs and local paths are supported for import. " +
|
||||
"Generic HTTP URLs are not supported. Use a GitHub URL (https://github.com/...) or a local directory path.",
|
||||
"Generic HTTP URLs are not supported. Use a GitHub or GitHub Enterprise URL (https://github.com/... or https://ghe.example.com/...) or a local directory path.",
|
||||
);
|
||||
}
|
||||
sourcePayload = { type: "github", url: normalizeGithubImportSource(from, opts.ref) };
|
||||
|
|
|
|||
|
|
@ -375,6 +375,7 @@ describe("company portability", () => {
|
|||
expect(
|
||||
parseGitHubSourceUrl("https://github.com/paperclipai/companies?ref=feature%2Fdemo&path=gstack"),
|
||||
).toEqual({
|
||||
hostname: "github.com",
|
||||
owner: "paperclipai",
|
||||
repo: "companies",
|
||||
ref: "feature/demo",
|
||||
|
|
@ -389,6 +390,7 @@ describe("company portability", () => {
|
|||
"https://github.com/paperclipai/companies?ref=abc123&companyPath=gstack%2FCOMPANY.md",
|
||||
),
|
||||
).toEqual({
|
||||
hostname: "github.com",
|
||||
owner: "paperclipai",
|
||||
repo: "companies",
|
||||
ref: "abc123",
|
||||
|
|
|
|||
|
|
@ -2100,8 +2100,16 @@ function parseFrontmatterMarkdown(raw: string): MarkdownDoc {
|
|||
};
|
||||
}
|
||||
|
||||
async function ghFetch(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 GitHub or GitHub Enterprise instance`);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchText(url: string) {
|
||||
const response = await fetch(url);
|
||||
const response = await ghFetch(url);
|
||||
if (!response.ok) {
|
||||
throw unprocessable(`Failed to fetch ${url}: ${response.status}`);
|
||||
}
|
||||
|
|
@ -2109,7 +2117,7 @@ async function fetchText(url: string) {
|
|||
}
|
||||
|
||||
async function fetchOptionalText(url: string) {
|
||||
const response = await fetch(url);
|
||||
const response = await ghFetch(url);
|
||||
if (response.status === 404) return null;
|
||||
if (!response.ok) {
|
||||
throw unprocessable(`Failed to fetch ${url}: ${response.status}`);
|
||||
|
|
@ -2118,7 +2126,7 @@ async function fetchOptionalText(url: string) {
|
|||
}
|
||||
|
||||
async function fetchBinary(url: string) {
|
||||
const response = await fetch(url);
|
||||
const response = await ghFetch(url);
|
||||
if (!response.ok) {
|
||||
throw unprocessable(`Failed to fetch ${url}: ${response.status}`);
|
||||
}
|
||||
|
|
@ -2126,7 +2134,7 @@ async function fetchBinary(url: string) {
|
|||
}
|
||||
|
||||
async function fetchJson<T>(url: string): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
const response = await ghFetch(url, {
|
||||
headers: {
|
||||
accept: "application/vnd.github+json",
|
||||
},
|
||||
|
|
@ -2411,14 +2419,16 @@ function buildManifestFromPackageFiles(
|
|||
const repoPath = asString(primarySource?.path);
|
||||
const commit = asString(primarySource?.commit);
|
||||
const trackingRef = asString(primarySource?.trackingRef);
|
||||
const sourceHostname = asString(primarySource?.hostname) || "github.com";
|
||||
const [owner, repoName] = (repo ?? "").split("/");
|
||||
sourceType = "github";
|
||||
sourceLocator = asString(primarySource?.url)
|
||||
?? (repo ? `https://github.com/${repo}${repoPath ? `/tree/${trackingRef ?? commit ?? "main"}/${repoPath}` : ""}` : null);
|
||||
?? (repo ? `https://${sourceHostname}/${repo}${repoPath ? `/tree/${trackingRef ?? commit ?? "main"}/${repoPath}` : ""}` : null);
|
||||
sourceRef = commit;
|
||||
normalizedMetadata = owner && repoName
|
||||
? {
|
||||
sourceKind: "github",
|
||||
...(sourceHostname !== "github.com" ? { hostname: sourceHostname } : {}),
|
||||
owner,
|
||||
repo: repoName,
|
||||
ref: commit,
|
||||
|
|
@ -2564,9 +2574,7 @@ function normalizeGitHubSourcePath(value: string | null | undefined) {
|
|||
|
||||
export function parseGitHubSourceUrl(rawUrl: string) {
|
||||
const url = new URL(rawUrl);
|
||||
if (url.hostname !== "github.com") {
|
||||
throw unprocessable("GitHub source must use github.com URL");
|
||||
}
|
||||
const hostname = url.hostname;
|
||||
const parts = url.pathname.split("/").filter(Boolean);
|
||||
if (parts.length < 2) {
|
||||
throw unprocessable("Invalid GitHub URL");
|
||||
|
|
@ -2584,6 +2592,7 @@ export function parseGitHubSourceUrl(rawUrl: string) {
|
|||
if (basePath === ".") basePath = "";
|
||||
}
|
||||
return {
|
||||
hostname,
|
||||
owner,
|
||||
repo,
|
||||
ref: queryRef || "main",
|
||||
|
|
@ -2607,12 +2616,23 @@ export function parseGitHubSourceUrl(rawUrl: string) {
|
|||
basePath = path.posix.dirname(blobPath);
|
||||
if (basePath === ".") basePath = "";
|
||||
}
|
||||
return { owner, repo, ref, basePath, companyPath };
|
||||
return { hostname, owner, repo, ref, basePath, companyPath };
|
||||
}
|
||||
|
||||
function resolveRawGitHubUrl(owner: string, repo: string, ref: string, filePath: string) {
|
||||
const normalizedFilePath = filePath.replace(/^\/+/, "");
|
||||
return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${normalizedFilePath}`;
|
||||
function isGitHubDotCom(hostname: string) {
|
||||
const h = hostname.toLowerCase();
|
||||
return h === "github.com" || h === "www.github.com";
|
||||
}
|
||||
|
||||
function resolveRawGitHubUrl(hostname: string, owner: string, repo: string, ref: string, filePath: string) {
|
||||
const p = filePath.replace(/^\/+/, "");
|
||||
return isGitHubDotCom(hostname)
|
||||
? `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${p}`
|
||||
: `https://${hostname}/raw/${owner}/${repo}/${ref}/${p}`;
|
||||
}
|
||||
|
||||
function gitHubApiBase(hostname: string) {
|
||||
return isGitHubDotCom(hostname) ? "https://api.github.com" : `https://${hostname}/api/v3`;
|
||||
}
|
||||
|
||||
export function companyPortabilityService(db: Db, storage?: StorageService) {
|
||||
|
|
@ -2641,14 +2661,14 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
|||
let companyMarkdown: string | null = null;
|
||||
try {
|
||||
companyMarkdown = await fetchOptionalText(
|
||||
resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, companyRelativePath),
|
||||
resolveRawGitHubUrl(parsed.hostname, parsed.owner, parsed.repo, ref, companyRelativePath),
|
||||
);
|
||||
} catch (err) {
|
||||
if (ref === "main") {
|
||||
ref = "master";
|
||||
warnings.push("GitHub ref main not found; falling back to master.");
|
||||
companyMarkdown = await fetchOptionalText(
|
||||
resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, companyRelativePath),
|
||||
resolveRawGitHubUrl(parsed.hostname, parsed.owner, parsed.repo, ref, companyRelativePath),
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
|
|
@ -2664,8 +2684,9 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
|||
const files: Record<string, CompanyPortabilityFileEntry> = {
|
||||
[companyPath]: companyMarkdown,
|
||||
};
|
||||
const apiBase = gitHubApiBase(parsed.hostname);
|
||||
const tree = await fetchJson<{ tree?: Array<{ path: string; type: string }> }>(
|
||||
`https://api.github.com/repos/${parsed.owner}/${parsed.repo}/git/trees/${ref}?recursive=1`,
|
||||
`${apiBase}/repos/${parsed.owner}/${parsed.repo}/git/trees/${ref}?recursive=1`,
|
||||
).catch(() => ({ tree: [] }));
|
||||
const basePrefix = parsed.basePath ? `${parsed.basePath.replace(/^\/+|\/+$/g, "")}/` : "";
|
||||
const candidatePaths = (tree.tree ?? [])
|
||||
|
|
@ -2686,7 +2707,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
|||
const relativePath = basePrefix ? repoPath.slice(basePrefix.length) : repoPath;
|
||||
if (files[relativePath] !== undefined) continue;
|
||||
files[normalizePortablePath(relativePath)] = await fetchText(
|
||||
resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, repoPath),
|
||||
resolveRawGitHubUrl(parsed.hostname, parsed.owner, parsed.repo, ref, repoPath),
|
||||
);
|
||||
}
|
||||
const companyDoc = parseFrontmatterMarkdown(companyMarkdown);
|
||||
|
|
@ -2697,7 +2718,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
|||
if (files[relativePath] !== undefined) continue;
|
||||
if (!(repoPath.endsWith(".md") || repoPath.endsWith(".yaml") || repoPath.endsWith(".yml"))) continue;
|
||||
files[relativePath] = await fetchText(
|
||||
resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, repoPath),
|
||||
resolveRawGitHubUrl(parsed.hostname, parsed.owner, parsed.repo, ref, repoPath),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2707,7 +2728,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
|
|||
const repoPath = [parsed.basePath, companyLogoPath].filter(Boolean).join("/");
|
||||
try {
|
||||
const binary = await fetchBinary(
|
||||
resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, repoPath),
|
||||
resolveRawGitHubUrl(parsed.hostname, parsed.owner, parsed.repo, ref, repoPath),
|
||||
);
|
||||
resolved.files[companyLogoPath] = bufferToPortableBinaryFile(binary, inferContentTypeFromPath(companyLogoPath));
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ type ParsedSkillImportSource = {
|
|||
type SkillSourceMeta = {
|
||||
skillKey?: string;
|
||||
sourceKind?: string;
|
||||
hostname?: string;
|
||||
owner?: string;
|
||||
repo?: string;
|
||||
ref?: string;
|
||||
|
|
@ -469,8 +470,16 @@ function parseFrontmatterMarkdown(raw: string): { frontmatter: Record<string, un
|
|||
};
|
||||
}
|
||||
|
||||
async function ghFetch(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 GitHub or GitHub Enterprise instance`);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchText(url: string) {
|
||||
const response = await fetch(url);
|
||||
const response = await ghFetch(url);
|
||||
if (!response.ok) {
|
||||
throw unprocessable(`Failed to fetch ${url}: ${response.status}`);
|
||||
}
|
||||
|
|
@ -478,7 +487,7 @@ async function fetchText(url: string) {
|
|||
}
|
||||
|
||||
async function fetchJson<T>(url: string): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
const response = await ghFetch(url, {
|
||||
headers: {
|
||||
accept: "application/vnd.github+json",
|
||||
},
|
||||
|
|
@ -489,16 +498,25 @@ async function fetchJson<T>(url: string): Promise<T> {
|
|||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async function resolveGitHubDefaultBranch(owner: string, repo: string) {
|
||||
function isGitHubDotCom(hostname: string) {
|
||||
const h = hostname.toLowerCase();
|
||||
return h === "github.com" || h === "www.github.com";
|
||||
}
|
||||
|
||||
function gitHubApiBase(hostname: string) {
|
||||
return isGitHubDotCom(hostname) ? "https://api.github.com" : `https://${hostname}/api/v3`;
|
||||
}
|
||||
|
||||
async function resolveGitHubDefaultBranch(owner: string, repo: string, apiBase: string) {
|
||||
const response = await fetchJson<{ default_branch?: string }>(
|
||||
`https://api.github.com/repos/${owner}/${repo}`,
|
||||
`${apiBase}/repos/${owner}/${repo}`,
|
||||
);
|
||||
return asString(response.default_branch) ?? "main";
|
||||
}
|
||||
|
||||
async function resolveGitHubCommitSha(owner: string, repo: string, ref: string) {
|
||||
async function resolveGitHubCommitSha(owner: string, repo: string, ref: string, apiBase: string) {
|
||||
const response = await fetchJson<{ sha?: string }>(
|
||||
`https://api.github.com/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`,
|
||||
`${apiBase}/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`,
|
||||
);
|
||||
const sha = asString(response.sha);
|
||||
if (!sha) {
|
||||
|
|
@ -509,9 +527,6 @@ async function resolveGitHubCommitSha(owner: string, repo: string, ref: string)
|
|||
|
||||
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");
|
||||
|
|
@ -532,10 +547,11 @@ function parseGitHubSourceUrl(rawUrl: string) {
|
|||
basePath = filePath ? path.posix.dirname(filePath) : "";
|
||||
explicitRef = true;
|
||||
}
|
||||
return { owner, repo, ref, basePath, filePath, explicitRef };
|
||||
return { hostname: url.hostname, owner, repo, ref, basePath, filePath, explicitRef };
|
||||
}
|
||||
|
||||
async function resolveGitHubPinnedRef(parsed: ReturnType<typeof parseGitHubSourceUrl>) {
|
||||
const apiBase = gitHubApiBase(parsed.hostname);
|
||||
if (/^[0-9a-f]{40}$/i.test(parsed.ref.trim())) {
|
||||
return {
|
||||
pinnedRef: parsed.ref,
|
||||
|
|
@ -545,13 +561,16 @@ async function resolveGitHubPinnedRef(parsed: ReturnType<typeof parseGitHubSourc
|
|||
|
||||
const trackingRef = parsed.explicitRef
|
||||
? parsed.ref
|
||||
: await resolveGitHubDefaultBranch(parsed.owner, parsed.repo);
|
||||
const pinnedRef = await resolveGitHubCommitSha(parsed.owner, parsed.repo, trackingRef);
|
||||
: await resolveGitHubDefaultBranch(parsed.owner, parsed.repo, apiBase);
|
||||
const pinnedRef = await resolveGitHubCommitSha(parsed.owner, parsed.repo, trackingRef, apiBase);
|
||||
return { pinnedRef, trackingRef };
|
||||
}
|
||||
|
||||
function resolveRawGitHubUrl(owner: string, repo: string, ref: string, filePath: string) {
|
||||
return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filePath.replace(/^\/+/, "")}`;
|
||||
function resolveRawGitHubUrl(hostname: string, owner: string, repo: string, ref: string, filePath: string) {
|
||||
const p = filePath.replace(/^\/+/, "");
|
||||
return isGitHubDotCom(hostname)
|
||||
? `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${p}`
|
||||
: `https://${hostname}/raw/${owner}/${repo}/${ref}/${p}`;
|
||||
}
|
||||
|
||||
function extractCommandTokens(raw: string) {
|
||||
|
|
@ -675,9 +694,10 @@ function deriveImportedSkillSource(
|
|||
const repoPath = asString(sourceEntry?.path);
|
||||
const commit = asString(sourceEntry?.commit);
|
||||
const trackingRef = asString(sourceEntry?.trackingRef);
|
||||
const sourceHostname = asString(sourceEntry?.hostname) || "github.com";
|
||||
const url = asString(sourceEntry?.url)
|
||||
?? (repo
|
||||
? `https://github.com/${repo}${repoPath ? `/tree/${trackingRef ?? commit ?? "main"}/${repoPath}` : ""}`
|
||||
? `https://${sourceHostname}/${repo}${repoPath ? `/tree/${trackingRef ?? commit ?? "main"}/${repoPath}` : ""}`
|
||||
: null);
|
||||
const [owner, repoName] = (repo ?? "").split("/");
|
||||
if (repo && owner && repoName) {
|
||||
|
|
@ -688,6 +708,7 @@ function deriveImportedSkillSource(
|
|||
metadata: {
|
||||
...(canonicalKey ? { skillKey: canonicalKey } : {}),
|
||||
sourceKind: "github",
|
||||
...(sourceHostname !== "github.com" ? { hostname: sourceHostname } : {}),
|
||||
owner,
|
||||
repo: repoName,
|
||||
ref: commit,
|
||||
|
|
@ -981,12 +1002,18 @@ async function readUrlSkillImports(
|
|||
): Promise<{ skills: ImportedSkill[]; warnings: string[] }> {
|
||||
const url = sourceUrl.trim();
|
||||
const warnings: string[] = [];
|
||||
if (url.includes("github.com/")) {
|
||||
const isGitHubRepoUrl = (() => { try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return false;
|
||||
return parsed.pathname.split("/").filter(Boolean).length >= 2 && !parsed.pathname.endsWith(".md");
|
||||
} catch { return false; } })();
|
||||
if (isGitHubRepoUrl) {
|
||||
const parsed = parseGitHubSourceUrl(url);
|
||||
const apiBase = gitHubApiBase(parsed.hostname);
|
||||
const { pinnedRef, trackingRef } = await resolveGitHubPinnedRef(parsed);
|
||||
let ref = pinnedRef;
|
||||
const tree = await fetchJson<{ tree?: Array<{ path: string; type: string }> }>(
|
||||
`https://api.github.com/repos/${parsed.owner}/${parsed.repo}/git/trees/${ref}?recursive=1`,
|
||||
`${apiBase}/repos/${parsed.owner}/${parsed.repo}/git/trees/${ref}?recursive=1`,
|
||||
).catch(() => {
|
||||
throw unprocessable(`Failed to read GitHub tree for ${url}`);
|
||||
});
|
||||
|
|
@ -1013,7 +1040,7 @@ async function readUrlSkillImports(
|
|||
const skills: ImportedSkill[] = [];
|
||||
for (const relativeSkillPath of skillPaths) {
|
||||
const repoSkillPath = basePrefix ? `${basePrefix}${relativeSkillPath}` : relativeSkillPath;
|
||||
const markdown = await fetchText(resolveRawGitHubUrl(parsed.owner, parsed.repo, ref, repoSkillPath));
|
||||
const markdown = await fetchText(resolveRawGitHubUrl(parsed.hostname, parsed.owner, parsed.repo, ref, repoSkillPath));
|
||||
const parsedMarkdown = parseFrontmatterMarkdown(markdown);
|
||||
const skillDir = path.posix.dirname(relativeSkillPath);
|
||||
const slug = deriveImportedSkillSlug(parsedMarkdown.frontmatter, path.posix.basename(skillDir));
|
||||
|
|
@ -1027,9 +1054,10 @@ async function readUrlSkillImports(
|
|||
const metadata = {
|
||||
...(skillKey ? { skillKey } : {}),
|
||||
sourceKind: "github",
|
||||
...(parsed.hostname !== "github.com" ? { hostname: parsed.hostname } : {}),
|
||||
owner: parsed.owner,
|
||||
repo: parsed.repo,
|
||||
ref: ref,
|
||||
ref,
|
||||
trackingRef,
|
||||
repoSkillDir: normalizeGitHubSkillDirectory(
|
||||
basePrefix ? `${basePrefix}${skillDir}` : skillDir,
|
||||
|
|
@ -1641,7 +1669,9 @@ export function companySkillService(db: Db) {
|
|||
};
|
||||
}
|
||||
|
||||
const latestRef = await resolveGitHubCommitSha(owner, repo, trackingRef);
|
||||
const hostname = asString(metadata.hostname) || "github.com";
|
||||
const apiBase = gitHubApiBase(hostname);
|
||||
const latestRef = await resolveGitHubCommitSha(owner, repo, trackingRef, apiBase);
|
||||
return {
|
||||
supported: true,
|
||||
reason: null,
|
||||
|
|
@ -1679,13 +1709,14 @@ export function companySkillService(db: Db) {
|
|||
const metadata = getSkillMeta(skill);
|
||||
const owner = asString(metadata.owner);
|
||||
const repo = asString(metadata.repo);
|
||||
const hostname = asString(metadata.hostname) || "github.com";
|
||||
const ref = skill.sourceRef ?? asString(metadata.ref) ?? "main";
|
||||
const repoSkillDir = normalizeGitHubSkillDirectory(asString(metadata.repoSkillDir), skill.slug);
|
||||
if (!owner || !repo) {
|
||||
throw unprocessable("Skill source metadata is incomplete.");
|
||||
}
|
||||
const repoPath = normalizePortablePath(path.posix.join(repoSkillDir, normalizedPath));
|
||||
content = await fetchText(resolveRawGitHubUrl(owner, repo, ref, repoPath));
|
||||
content = await fetchText(resolveRawGitHubUrl(hostname, owner, repo, ref, repoPath));
|
||||
} else if (skill.sourceType === "url") {
|
||||
if (normalizedPath !== "SKILL.md") {
|
||||
throw notFound("This skill source only exposes SKILL.md");
|
||||
|
|
|
|||
|
|
@ -121,8 +121,7 @@ export function NewProjectDialog() {
|
|||
const isGitHubRepoUrl = (value: string) => {
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
const host = parsed.hostname.toLowerCase();
|
||||
if (host !== "github.com" && host !== "www.github.com") return false;
|
||||
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return false;
|
||||
const segments = parsed.pathname.split("/").filter(Boolean);
|
||||
return segments.length >= 2;
|
||||
} catch {
|
||||
|
|
@ -157,7 +156,7 @@ export function NewProjectDialog() {
|
|||
return;
|
||||
}
|
||||
if (repoUrl && !isGitHubRepoUrl(repoUrl)) {
|
||||
setWorkspaceError("Repo must use a valid GitHub repo URL.");
|
||||
setWorkspaceError("Repo must use a valid GitHub or GitHub Enterprise repo URL.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -346,8 +346,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
|||
const isGitHubRepoUrl = (value: string) => {
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
const host = parsed.hostname.toLowerCase();
|
||||
if (host !== "github.com" && host !== "www.github.com") return false;
|
||||
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return false;
|
||||
const segments = parsed.pathname.split("/").filter(Boolean);
|
||||
return segments.length >= 2;
|
||||
} catch {
|
||||
|
|
@ -433,7 +432,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
|||
return;
|
||||
}
|
||||
if (!isGitHubRepoUrl(repoUrl)) {
|
||||
setWorkspaceError("Repo must use a valid GitHub repo URL.");
|
||||
setWorkspaceError("Repo must use a valid GitHub or GitHub Enterprise repo URL.");
|
||||
return;
|
||||
}
|
||||
setWorkspaceError(null);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue