Files
paseo/packages/protocol/src/git-remote.ts
Mohamed Boudra 53c14d9855 Extract client SDK package (#1052)
* Extract client SDK package

* Polish SDK client identity defaults

* Build client before dependent CI jobs

* Restore daemon client server export

* Extract protocol and client SDK packages

* Fix provider override schema validation

* Fix app test daemon client imports

* Simplify workspace build targets

* Fix CLI test server build bootstrap

* Run SDK package tests in CI

* Fix rebase package split drift

* Restore lockfile registry metadata

* Update SDK config test for prompt default

* Move terminal stream router test to client package

* Fix rebase drift for protocol imports

* Fix SDK agent capability fixture

* Restore legacy server client exports

* Fix server export compatibility test

* Advertise custom mode icon client capability

* Remove server daemon-client exports

* Format rebased mode control import

* Fix rebase drift for protocol imports

Files added by upstream PRs (#893, #1147, #1154) referenced the pre-split
shared/ paths that this branch moves into @getpaseo/protocol. Redirect
those imports to the protocol package so typecheck stays green after the
rebase.
2026-05-28 01:58:18 +08:00

87 lines
2.4 KiB
TypeScript

const GITHUB_HOSTS = new Set(["github.com", "ssh.github.com"]);
const TRANSPORT_BY_PROTOCOL: Record<string, GitRemoteLocation["transport"]> = {
"https:": "https",
"http:": "http",
"ssh:": "ssh",
};
export interface GitRemoteLocation {
transport: "scp" | "ssh" | "http" | "https";
host: string;
path: string;
}
export interface GitHubRemoteIdentity {
owner: string;
name: string;
repo: string;
}
export function parseGitHubRemoteUrl(remoteUrl: string): GitHubRemoteIdentity | null {
const location = parseGitRemoteLocation(remoteUrl);
if (!location || !isGitHubHost(location.host)) return null;
return parseGitHubRemoteIdentity(location.path);
}
export function parseGitRemoteLocation(remoteUrl: string): GitRemoteLocation | null {
const trimmed = remoteUrl.trim();
if (!trimmed) return null;
const scpLike = trimmed.match(/^[^@]+@([^:]+):(.+)$/u);
if (scpLike) {
const host = normalizeHost(scpLike[1] ?? "");
const path = normalizeRemotePath(scpLike[2] ?? "");
if (!isValidRemoteHost(host) || !path) return null;
return { transport: "scp", host, path };
}
let parsed: URL;
try {
parsed = new URL(trimmed);
} catch {
return null;
}
const transport = TRANSPORT_BY_PROTOCOL[parsed.protocol.toLowerCase()];
if (!transport) return null;
const host = normalizeHost(parsed.hostname);
let path: string;
try {
path = decodeURIComponent(parsed.pathname);
} catch {
return null;
}
const normalizedPath = normalizeRemotePath(path);
if (!isValidRemoteHost(host) || !normalizedPath) return null;
return { transport, host, path: normalizedPath };
}
export function parseGitHubRemoteIdentity(path: string): GitHubRemoteIdentity | null {
const segments = path.split("/").filter(Boolean);
if (segments.length !== 2) return null;
const [owner, name] = segments;
if (!owner || !name) return null;
return { owner, name, repo: `${owner}/${name}` };
}
export function isGitHubHost(host: string): boolean {
return GITHUB_HOSTS.has(host);
}
export function normalizeHost(host: string): string {
return host.trim().replace(/\.+$/u, "").toLowerCase();
}
function normalizeRemotePath(path: string): string | null {
let normalized = path.trim().replace(/^\/+|\/+$/gu, "");
if (normalized.endsWith(".git")) normalized = normalized.slice(0, -4);
return normalized || null;
}
function isValidRemoteHost(host: string): boolean {
return /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/u.test(host);
}