Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
7.9 kB · 272 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273import { existsSync, readFileSync, statSync } from "node:fs";import { dirname, join, resolve } from "node:path";import hostedGitInfo from "hosted-git-info";import { spawnSyncHidden } from "./child-process.js";
/** * 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;};
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}`; 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 parseGenericGitUrl(url: string): GitSource | null { 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}`; }
const normalizedPath = path.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 all historical shorthand forms. * - Without git: prefix, only accept explicit protocol URLs. */export function parseGitUrl(source: string): GitSource | null { 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): value is string => 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 { type: "git", repo: useHttpsPrefix ? `https://${split.repo}` : split.repo, host: info.domain || "", path: `${info.user}/${info.project}`.replace(/\.git$/, ""), ref: info.committish || split.ref || undefined, pinned: Boolean(info.committish || split.ref), }; } }
const httpsCandidates = [split.ref ? `https://${split.repo}#${split.ref}` : undefined, `https://${url}`].filter( (value): value is string => Boolean(value), ); for (const candidate of httpsCandidates) { const info = hostedGitInfo.fromUrl(candidate); if (info) { if (split.ref && info.project?.includes("@")) { continue; } return { type: "git", repo: `https://${split.repo}`, host: info.domain || "", path: `${info.user}/${info.project}`.replace(/\.git$/, ""), ref: info.committish || split.ref || undefined, pinned: Boolean(info.committish || split.ref), }; } }
return parseGenericGitUrl(url);}
export type GitPaths = { repoDir: string; commonGitDir: string; headPath: string;};
/** * Find git metadata paths by walking up from cwd. * Handles both regular git repos (.git is a directory) and worktrees (.git is a file). */export function findGitPaths(cwd: string): GitPaths | null { let dir = cwd; while (true) { const gitPath = join(dir, ".git"); if (existsSync(gitPath)) { try { const stat = statSync(gitPath); if (stat.isFile()) { const content = readFileSync(gitPath, "utf8").trim(); if (content.startsWith("gitdir: ")) { const gitDir = resolve(dir, content.slice(8).trim()); const headPath = join(gitDir, "HEAD"); if (!existsSync(headPath)) return null; const commonDirPath = join(gitDir, "commondir"); const commonGitDir = existsSync(commonDirPath) ? resolve(gitDir, readFileSync(commonDirPath, "utf8").trim()) : gitDir; return { repoDir: dir, commonGitDir, headPath }; } } else if (stat.isDirectory()) { const headPath = join(gitPath, "HEAD"); if (!existsSync(headPath)) return null; return { repoDir: dir, commonGitDir: gitPath, headPath }; } } catch { return null; } } const parent = dirname(dir); if (parent === dir) return null; dir = parent; }}
export interface GitContext { repoUrl?: string; commit?: string; branch?: string;}
export function gitContextsEqual(a: GitContext, b: GitContext): boolean { return a.repoUrl === b.repoUrl && a.commit === b.commit && a.branch === b.branch;}
function runGit(cwd: string, args: string[]): string | null { const result = spawnSyncHidden("git", ["--no-optional-locks", ...args], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }); if (result.status !== 0 || typeof result.stdout !== "string") return null; return result.stdout.trim() || null;}
export function captureGitContext(cwd: string): GitContext | null { const commit = runGit(cwd, ["rev-parse", "HEAD"]); const branch = runGit(cwd, ["branch", "--show-current"]); const remote = runGit(cwd, ["remote", "get-url", "origin"]); if (!commit && !branch && !remote) return null;
const context: GitContext = {}; if (remote) context.repoUrl = parseGitUrl(remote)?.repo ?? remote; if (commit) context.commit = commit; if (branch) context.branch = branch; return context;}