openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
107 lines (106 loc) • 3.88 kB
JavaScript
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 };