Files
oh-my-pi/packages/coding-agent/src/extensibility/plugins/git-url.ts
T
can1357 c12be01a5f fix(coding-agent): backported pi-mono changes (34878e..5133697)
packages/ai:
- fix: hardened OpenAI tool-call JSON parsing for malformed trailing arguments
- feat: routed GitHub Copilot Claude 4.x models through anthropic-messages
- feat: centralized dynamic Copilot headers and anthropic bearer auth handling
- feat: added optional StreamOptions.metadata propagation
- test: added Copilot headers/auth/routing coverage
- fix: updated model generator and models.json for Copilot Claude API mapping

packages/coding-agent:
- fix: made CLI model resolution deterministic with provider-aware pattern parsing
- fix: corrected compaction boundary/context usage handling after compaction
- feat: expanded extension events and terminal input hook integration
- fix: hardened git source parsing to avoid local-path misclassification
- test: added git-url parser coverage and model-resolver cases

packages/tui:
- fix: scoped @ fuzzy autocomplete to typed path prefixes
- feat: added Windows VT input mode support via bun:ffi

docs:
- chore: updated porting sync point to 5133697
2026-02-16 10:08:53 +01:00

282 lines
8.5 KiB
TypeScript

/**
* Parsed git URL information.
*/
export type GitSource = {
/** Always "git" for git sources */
type: "git";
/** Clone URL (always valid for git clone, without ref suffix) */
repo: string;
/** Git host domain (e.g., "github.com") */
host: string;
/** Repository path (e.g., "user/repo") */
path: string;
/** Git ref (branch, tag, commit) if specified */
ref?: string;
/** True if ref was specified (package won't be auto-updated) */
pinned: boolean;
};
/** Known git hosts and their URL extraction logic. */
const KNOWN_HOSTS: Record<string, (pathname: string, hash: string) => { user: string; project: string } | null> = {
"github.com": extractStandard,
"gitlab.com": extractGitLab,
"bitbucket.org": extractStandard,
"git.sr.ht": extractStandard,
"codeberg.org": extractStandard,
};
function stripUrlCredentials(url: string): string {
if (!url.includes("://")) return url;
try {
const parsed = new URL(url);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return url;
if (!parsed.username && !parsed.password) return url;
parsed.username = "";
parsed.password = "";
return parsed.toString().replace(/\/$/, "");
} catch {
return url;
}
}
function extractStandard(pathname: string, _hash: string): { user: string; project: string } | null {
const [, user, project] = pathname.split("/", 3);
if (!user || !project) return null;
return { user, project: project.replace(/\.git$/, "") };
}
function extractGitLab(pathname: string, _hash: string): { user: string; project: string } | null {
const path = pathname.startsWith("/") ? pathname.slice(1) : pathname;
if (path.includes("/-/") || path.includes("/archive.tar.gz")) return null;
const segments = path.split("/");
let project = segments.pop();
if (!project) return null;
project = project.replace(/\.git$/, "");
const user = segments.join("/");
if (!user || !project) return null;
return { user, project };
}
/**
* Try to parse a URL against known git hosts.
* Returns `{ domain, user, project, committish }` or null.
*/
function tryKnownHost(candidate: string): { domain: string; user: string; project: string; committish: string } | null {
let parsed: URL;
try {
parsed = new URL(candidate);
} catch {
return null;
}
const hostname = parsed.hostname.startsWith("www.") ? parsed.hostname.slice(4) : parsed.hostname;
const extractor = KNOWN_HOSTS[hostname];
if (!extractor) return null;
const segments = extractor(parsed.pathname, parsed.hash);
if (!segments) return null;
let committish = "";
if (parsed.hash) {
try {
committish = decodeURIComponent(parsed.hash.slice(1));
} catch {
return null;
}
}
return {
domain: hostname,
user: segments.user,
project: segments.project,
committish,
};
}
function splitRef(url: string): { repo: string; ref?: string } {
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}`;
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
parsed.username = "";
parsed.password = "";
}
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 };
}
/** Try known-host parsing and build a GitSource from the result. */
function tryKnownHostSource(
split: { repo: string; ref?: string },
candidate: string,
repoUrl: string,
): GitSource | null {
const info = tryKnownHost(candidate);
if (!info) return null;
if (split.ref && info.project.includes("@")) return null;
return {
type: "git",
repo: stripUrlCredentials(repoUrl),
host: info.domain,
path: `${info.user}/${info.project}`.replace(/\.git$/, ""),
ref: info.committish || split.ref || undefined,
pinned: Boolean(info.committish || split.ref),
};
}
function parseGenericGitUrl(url: string): GitSource | null {
const { repo: repoWithoutRef, ref } = splitRef(url);
let repo = repoWithoutRef;
let host = "";
let repoPath = "";
const scpLikeMatch = repoWithoutRef.match(/^git@([^:]+):(.+)$/);
if (scpLikeMatch) {
host = scpLikeMatch[1] ?? "";
repoPath = scpLikeMatch[2] ?? "";
} else if (/^https?:\/\/|^ssh:\/\/|^git:\/\//.test(repoWithoutRef)) {
try {
const parsed = new URL(repoWithoutRef);
if (parsed.hash) {
try {
decodeURIComponent(parsed.hash.slice(1));
} catch {
return null;
}
}
host = parsed.hostname;
repoPath = parsed.pathname.replace(/^\/+/, "");
repo = stripUrlCredentials(repoWithoutRef);
} catch {
return null;
}
} else {
const slashIndex = repoWithoutRef.indexOf("/");
if (slashIndex < 0) return null;
repo = `https://${repoWithoutRef}`;
try {
const parsed = new URL(repo);
host = parsed.hostname;
repoPath = parsed.pathname.replace(/^\/+/, "");
repo = stripUrlCredentials(repo);
} catch {
return null;
}
if (!host.includes(".") && host !== "localhost") return null;
}
const normalizedPath = repoPath.replace(/\.git$/, "").replace(/^\/+/, "");
if (!host || !normalizedPath || normalizedPath.split("/").length < 2) return null;
return { type: "git", repo, host, path: normalizedPath, ref, pinned: Boolean(ref) };
}
/**
* Parse git source into a GitSource.
*
* Rules:
* - With `git:` prefix, accept shorthand forms.
* - Without `git:` prefix, only accept explicit protocol URLs.
*
* Handles:
* - `git:` prefixed URLs (`git:github.com/user/repo`)
* - SSH SCP-like URLs (`git:git@github.com:user/repo`)
* - HTTPS/HTTP/SSH/git protocol URLs
* - Ref pinning via `@ref` suffix
*
* Recognizes GitHub, GitLab, Bitbucket, Sourcehut, and Codeberg natively.
* Falls back to generic URL parsing for other hosts.
*/
export function parseGitUrl(source: string): GitSource | null {
const trimmed = source.trim();
const hasGitPrefix = /^git:(?!\/\/)/i.test(trimmed);
const url = hasGitPrefix ? trimmed.slice(4).trim() : trimmed;
if (!hasGitPrefix && !/^(https?|ssh|git):\/\//i.test(url)) {
return null;
}
const hashIndex = url.indexOf("#");
if (hashIndex >= 0) {
const hash = url.slice(hashIndex + 1);
if (hash) {
try {
decodeURIComponent(hash);
} catch {
return null;
}
}
}
const split = splitRef(url);
// SCP-like SSH URLs (git@host:user/repo) — convert to https for host matching
const scpMatch = split.repo.match(/^git@([^:]+):(.+)$/);
// Try known hosts with the repo URL directly
const directCandidates: string[] = [];
if (scpMatch) {
directCandidates.push(`https://${scpMatch[1]}/${scpMatch[2]}`);
} else if (/^https?:\/\/|^ssh:\/\/|^git:\/\//.test(split.repo)) {
directCandidates.push(split.repo);
}
for (const candidate of directCandidates) {
const withRef = split.ref ? `${candidate.replace(/#.*$/, "")}#${split.ref}` : candidate;
const needsHttps =
!split.repo.startsWith("http://") &&
!split.repo.startsWith("https://") &&
!split.repo.startsWith("ssh://") &&
!split.repo.startsWith("git://") &&
!split.repo.startsWith("git@");
const result = tryKnownHostSource(split, withRef, needsHttps ? `https://${split.repo}` : split.repo);
if (result) return result;
}
// Try with https:// prefix for bare host/user/repo shorthand
if (!split.repo.includes("://") && !split.repo.startsWith("git@")) {
const httpsCandidate = split.ref ? `https://${split.repo}#${split.ref}` : `https://${url}`;
const result = tryKnownHostSource(split, httpsCandidate, `https://${split.repo}`);
if (result) return result;
}
return parseGenericGitUrl(url);
}