UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

107 lines (106 loc) • 3.88 kB
import hostedGitInfo from "hosted-git-info"; //#region src/agents/utils/git.ts /** * Git source parsing helpers. * * Normalizes git-style package references into host/path identity. */ function stripRef(url) { const protocolIndex = url.indexOf("://"); const pathStart = url.startsWith("git@") ? url.indexOf(":") + 1 : url.indexOf("/", protocolIndex < 0 ? 0 : protocolIndex + 3) + 1; if (pathStart <= 0) return url; const suffixOffset = url.slice(pathStart).search(/[?#]/); const pathEnd = suffixOffset < 0 ? url.length : pathStart + suffixOffset; let refSeparator = url.indexOf("@", pathStart); if (refSeparator === pathStart) refSeparator = url.indexOf("@", pathStart + 1); if (refSeparator <= pathStart || refSeparator === pathEnd - 1 || refSeparator >= pathEnd) return url; return protocolIndex < 0 ? url.slice(0, refSeparator) : url.slice(0, refSeparator) + url.slice(pathEnd); } function hasUnsafePathSegments(url) { const path = url.split(/[?#]/, 1)[0] ?? ""; return path.includes("\\") || /(?:^|\/)(?:\.|%2e){1,2}(?:\/|$)/i.test(path); } function parseGenericGitUrl(url) { let host; let path; const scpLikeMatch = url.match(/^git@([^:]+):(.+)$/); if (scpLikeMatch) { host = scpLikeMatch[1] ?? ""; path = scpLikeMatch[2] ?? ""; } else if (/^(?:https?|ssh|git):\/\//.test(url)) try { const parsed = new URL(url); host = parsed.hostname; path = parsed.pathname.replace(/^\/+/, ""); } catch { return null; } else { const slashIndex = url.indexOf("/"); if (slashIndex < 0) return null; host = url.slice(0, slashIndex); path = url.slice(slashIndex + 1); if (!host.includes(".") && host !== "localhost") return null; } const normalizedPath = normalizeGitPath(path); if (!isSafeGitHost(host) || !normalizedPath) return null; return { type: "git", host, path: normalizedPath }; } function isSafeGitHost(host) { return Boolean(host) && !host.includes("/") && !host.includes("\\") && host !== "." && host !== ".."; } function normalizeGitPath(path) { const segments = path.replace(/\.git$/, "").replace(/^\/+/, "").split("/"); if (segments.length < 2) return null; if (segments.some((segment) => !segment || segment === "." || segment === ".." || segment.includes("\\"))) return null; return segments.join("/"); } function parseHostedGitUrl(url) { const candidates = [url]; if (!url.includes("://") && !url.startsWith("git@")) candidates.push(`https://${url}`); for (const candidate of candidates) { const info = hostedGitInfo.fromUrl(candidate); if (!info) continue; const host = info.domain || ""; const path = normalizeGitPath(`${info.user}/${info.project}`); if (isSafeGitHost(host) && path) return { type: "git", host, path }; } return null; } /** * Parse git source into a GitSource. * * Rules: * - With git: prefix, accept all historical shorthand forms. * - Without git: prefix, only accept explicit protocol URLs. */ function parseGitUrl(source) { const trimmed = source.trim(); const hasGitPrefix = trimmed.startsWith("git:"); const url = hasGitPrefix ? trimmed.slice(4).trim() : trimmed; if (!hasGitPrefix && !/^(https?|ssh|git):\/\//i.test(url)) return null; const urlWithoutRef = stripRef(url); if (hasUnsafePathSegments(urlWithoutRef)) return null; return parseHostedGitUrl(urlWithoutRef) ?? parseGenericGitUrl(urlWithoutRef); } //#endregion //#region src/config/cloud-worker-project-profiles.ts /** Normalize a Git origin URL to a lowercase host/path repository identity. */ function normalizeCloudRepo(originUrl) { const value = originUrl.trim(); if (!value) return; const source = parseGitUrl(`git:${value}`); if (!source) return; const segments = source.path.split("/").filter(Boolean); if (!source.host || segments.length < 2) return; return `${source.host}/${segments.join("/")}`.toLowerCase(); } //#endregion export { parseGitUrl as n, normalizeCloudRepo as t };