@earendil-works/pi-coding-agent
Version:
Coding agent CLI with read, bash, edit, write tools and session management
195 lines • 6.6 kB
JavaScript
import hostedGitInfo from "hosted-git-info";
function splitRef(url) {
const scpLikeMatch = url.match(/^git@([^:]+):(.+)$/);
if (scpLikeMatch) {
const pathWithMaybeRef = scpLikeMatch[2] ?? "";
const refSeparator = pathWithMaybeRef.indexOf("@");
if (refSeparator < 0)
return { repo: url };
const repoPath = pathWithMaybeRef.slice(0, refSeparator);
const ref = pathWithMaybeRef.slice(refSeparator + 1);
if (!repoPath || !ref)
return { repo: url };
return {
repo: `git@${scpLikeMatch[1] ?? ""}:${repoPath}`,
ref,
};
}
if (url.includes("://")) {
try {
const parsed = new URL(url);
const pathWithMaybeRef = parsed.pathname.replace(/^\/+/, "");
const refSeparator = pathWithMaybeRef.indexOf("@");
if (refSeparator < 0)
return { repo: url };
const repoPath = pathWithMaybeRef.slice(0, refSeparator);
const ref = pathWithMaybeRef.slice(refSeparator + 1);
if (!repoPath || !ref)
return { repo: url };
parsed.pathname = `/${repoPath}`;
return {
repo: parsed.toString().replace(/\/$/, ""),
ref,
};
}
catch {
return { repo: url };
}
}
const slashIndex = url.indexOf("/");
if (slashIndex < 0) {
return { repo: url };
}
const host = url.slice(0, slashIndex);
const pathWithMaybeRef = url.slice(slashIndex + 1);
const refSeparator = pathWithMaybeRef.indexOf("@");
if (refSeparator < 0) {
return { repo: url };
}
const repoPath = pathWithMaybeRef.slice(0, refSeparator);
const ref = pathWithMaybeRef.slice(refSeparator + 1);
if (!repoPath || !ref) {
return { repo: url };
}
return {
repo: `${host}/${repoPath}`,
ref,
};
}
function decodeForValidation(value) {
try {
return decodeURIComponent(value);
}
catch {
return null;
}
}
function hasUnsafeGitInstallPart(value, allowSlash) {
const decoded = decodeForValidation(value);
if (decoded === null) {
return true;
}
const candidates = [value, decoded];
for (const candidate of candidates) {
if (candidate.includes("\0") || candidate.includes("\\") || candidate.startsWith("/")) {
return true;
}
if (!allowSlash && candidate.includes("/")) {
return true;
}
if (candidate.split("/").includes("..")) {
return true;
}
}
return false;
}
function buildGitSource(args) {
if (args.path.startsWith("/")) {
return null;
}
const normalizedPath = args.path.replace(/\.git$/, "").replace(/^\/+/, "");
if (!args.host || !normalizedPath || normalizedPath.split("/").length < 2) {
return null;
}
if (hasUnsafeGitInstallPart(args.host, false) || hasUnsafeGitInstallPart(normalizedPath, true)) {
return null;
}
return {
type: "git",
repo: args.repo,
host: args.host,
path: normalizedPath,
ref: args.ref,
pinned: Boolean(args.ref),
};
}
function parseGenericGitUrl(url) {
const { repo: repoWithoutRef, ref } = splitRef(url);
let repo = repoWithoutRef;
let host = "";
let path = "";
const scpLikeMatch = repoWithoutRef.match(/^git@([^:]+):(.+)$/);
if (scpLikeMatch) {
host = scpLikeMatch[1] ?? "";
path = scpLikeMatch[2] ?? "";
}
else if (repoWithoutRef.startsWith("https://") ||
repoWithoutRef.startsWith("http://") ||
repoWithoutRef.startsWith("ssh://") ||
repoWithoutRef.startsWith("git://")) {
try {
const parsed = new URL(repoWithoutRef);
host = parsed.hostname;
path = parsed.pathname.replace(/^\/+/, "");
}
catch {
return null;
}
}
else {
const slashIndex = repoWithoutRef.indexOf("/");
if (slashIndex < 0) {
return null;
}
host = repoWithoutRef.slice(0, slashIndex);
path = repoWithoutRef.slice(slashIndex + 1);
if (!host.includes(".") && host !== "localhost") {
return null;
}
repo = `https://${repoWithoutRef}`;
}
return buildGitSource({ repo, host, path, ref });
}
/**
* Parse git source into a GitSource.
*
* Rules:
* - With git: prefix, accept all historical shorthand forms.
* - Without git: prefix, only accept explicit protocol URLs.
*/
export 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 split = splitRef(url);
const hostedCandidates = [split.ref ? `${split.repo}#${split.ref}` : undefined, url].filter((value) => Boolean(value));
for (const candidate of hostedCandidates) {
const info = hostedGitInfo.fromUrl(candidate);
if (info) {
if (split.ref && info.project?.includes("@")) {
continue;
}
const useHttpsPrefix = !split.repo.startsWith("http://") &&
!split.repo.startsWith("https://") &&
!split.repo.startsWith("ssh://") &&
!split.repo.startsWith("git://") &&
!split.repo.startsWith("git@");
return buildGitSource({
repo: useHttpsPrefix ? `https://${split.repo}` : split.repo,
host: info.domain || "",
path: `${info.user}/${info.project}`,
ref: info.committish || split.ref || undefined,
});
}
}
const httpsCandidates = [split.ref ? `https://${split.repo}#${split.ref}` : undefined, `https://${url}`].filter((value) => Boolean(value));
for (const candidate of httpsCandidates) {
const info = hostedGitInfo.fromUrl(candidate);
if (info) {
if (split.ref && info.project?.includes("@")) {
continue;
}
return buildGitSource({
repo: `https://${split.repo}`,
host: info.domain || "",
path: `${info.user}/${info.project}`,
ref: info.committish || split.ref || undefined,
});
}
}
return parseGenericGitUrl(url);
}
//# sourceMappingURL=git.js.map