mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 17:00:59 +00:00
Merge pull request #1287 from markusn/feature/ssh-git-packages
feat(coding-agent): add SSH URL support for git packages
This commit is contained in:
commit
6c6d937b2d
9 changed files with 576 additions and 48 deletions
|
|
@ -6,7 +6,7 @@ import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|||
import ignore from "ignore";
|
||||
import { minimatch } from "minimatch";
|
||||
import { CONFIG_DIR_NAME } from "../config.js";
|
||||
import { looksLikeGitUrl } from "../utils/git.js";
|
||||
import { type GitSource, parseGitUrl } from "../utils/git.js";
|
||||
import type { PackageSource, SettingsManager } from "./settings-manager.js";
|
||||
|
||||
export interface PathMetadata {
|
||||
|
|
@ -68,15 +68,6 @@ type NpmSource = {
|
|||
pinned: boolean;
|
||||
};
|
||||
|
||||
type GitSource = {
|
||||
type: "git";
|
||||
repo: string;
|
||||
host: string;
|
||||
path: string;
|
||||
ref?: string;
|
||||
pinned: boolean;
|
||||
};
|
||||
|
||||
type LocalSource = {
|
||||
type: "local";
|
||||
path: string;
|
||||
|
|
@ -894,21 +885,10 @@ export class DefaultPackageManager implements PackageManager {
|
|||
};
|
||||
}
|
||||
|
||||
if (source.startsWith("git:") || looksLikeGitUrl(source)) {
|
||||
const repoSpec = source.startsWith("git:") ? source.slice("git:".length).trim() : source;
|
||||
const [repo, ref] = repoSpec.split("@");
|
||||
const normalized = repo.replace(/^https?:\/\//, "").replace(/\.git$/, "");
|
||||
const parts = normalized.split("/");
|
||||
const host = parts.shift() ?? "";
|
||||
const repoPath = parts.join("/");
|
||||
return {
|
||||
type: "git",
|
||||
repo: normalized,
|
||||
host,
|
||||
path: repoPath,
|
||||
ref,
|
||||
pinned: Boolean(ref),
|
||||
};
|
||||
// Try parsing as git URL
|
||||
const gitParsed = parseGitUrl(source);
|
||||
if (gitParsed) {
|
||||
return gitParsed;
|
||||
}
|
||||
|
||||
return { type: "local", path: source };
|
||||
|
|
@ -961,6 +941,8 @@ export class DefaultPackageManager implements PackageManager {
|
|||
/**
|
||||
* Get a unique identity for a package, ignoring version/ref.
|
||||
* Used to detect when the same package is in both global and project settings.
|
||||
* For git packages, uses normalized host/path to ensure SSH and HTTPS URLs
|
||||
* for the same repository are treated as identical.
|
||||
*/
|
||||
private getPackageIdentity(source: string, scope?: SourceScope): string {
|
||||
const parsed = this.parseSource(source);
|
||||
|
|
@ -968,7 +950,8 @@ export class DefaultPackageManager implements PackageManager {
|
|||
return `npm:${parsed.name}`;
|
||||
}
|
||||
if (parsed.type === "git") {
|
||||
return `git:${parsed.repo}`;
|
||||
// Use host/path for identity to normalize SSH and HTTPS
|
||||
return `git:${parsed.host}/${parsed.path}`;
|
||||
}
|
||||
if (scope) {
|
||||
const baseDir = this.getBaseDirForScope(scope);
|
||||
|
|
@ -1046,8 +1029,8 @@ export class DefaultPackageManager implements PackageManager {
|
|||
this.ensureGitIgnore(gitRoot);
|
||||
}
|
||||
mkdirSync(dirname(targetDir), { recursive: true });
|
||||
const cloneUrl = source.repo.startsWith("http") ? source.repo : `https://${source.repo}`;
|
||||
await this.runCommand("git", ["clone", cloneUrl, targetDir]);
|
||||
|
||||
await this.runCommand("git", ["clone", source.repo, targetDir]);
|
||||
if (source.ref) {
|
||||
await this.runCommand("git", ["checkout", source.ref], { cwd: targetDir });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import { allTools } from "./core/tools/index.js";
|
|||
import { runMigrations, showDeprecationWarnings } from "./migrations.js";
|
||||
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
|
||||
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
|
||||
import { parseGitUrl } from "./utils/git.js";
|
||||
|
||||
/**
|
||||
* Read all content from piped stdin.
|
||||
|
|
@ -122,15 +123,13 @@ function normalizeExtensionSource(source: string): { type: "npm" | "git" | "loca
|
|||
const match = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@.+)?$/);
|
||||
return { type: "npm", key: match?.[1] ?? spec };
|
||||
}
|
||||
if (source.startsWith("git:")) {
|
||||
const repo = source.slice("git:".length).trim().split("@")[0] ?? "";
|
||||
return { type: "git", key: repo.replace(/^https?:\/\//, "").replace(/\.git$/, "") };
|
||||
}
|
||||
// Raw git URLs
|
||||
if (source.startsWith("https://") || source.startsWith("http://")) {
|
||||
const repo = source.split("@")[0] ?? "";
|
||||
return { type: "git", key: repo.replace(/^https?:\/\//, "").replace(/\.git$/, "") };
|
||||
|
||||
// Try parsing as git URL
|
||||
const parsed = parseGitUrl(source);
|
||||
if (parsed) {
|
||||
return { type: "git", key: `${parsed.host}/${parsed.path}` };
|
||||
}
|
||||
|
||||
return { type: "local", key: source };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,71 @@
|
|||
const GIT_HOSTS = ["github.com", "gitlab.com", "bitbucket.org", "codeberg.org"];
|
||||
import hostedGitInfo from "hosted-git-info";
|
||||
|
||||
export function looksLikeGitUrl(source: string): boolean {
|
||||
const normalized = source.replace(/^https?:\/\//, "");
|
||||
return GIT_HOSTS.some((host) => normalized.startsWith(`${host}/`));
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse any git URL (SSH or HTTPS) into a GitSource.
|
||||
*/
|
||||
export function parseGitUrl(source: string): GitSource | null {
|
||||
let url = source.startsWith("git:") ? source.slice(4).trim() : source;
|
||||
|
||||
// Try hosted-git-info, converting @ref to #ref if needed
|
||||
let info = hostedGitInfo.fromUrl(url);
|
||||
const lastAt = url.lastIndexOf("@");
|
||||
if ((info?.project?.includes("@") || !info) && lastAt > 0) {
|
||||
info = hostedGitInfo.fromUrl(`${url.slice(0, lastAt)}#${url.slice(lastAt + 1)}`) ?? info;
|
||||
url = url.slice(0, lastAt); // strip ref from url for repo field
|
||||
}
|
||||
|
||||
// Try with https:// prefix for shorthand URLs
|
||||
if (!info) {
|
||||
info = hostedGitInfo.fromUrl(`https://${url}`);
|
||||
if (info) url = `https://${url}`; // make repo a valid clone URL
|
||||
}
|
||||
|
||||
if (info) {
|
||||
return {
|
||||
type: "git",
|
||||
repo: url,
|
||||
host: info.domain || "",
|
||||
path: `${info.user}/${info.project}`,
|
||||
ref: info.committish || undefined,
|
||||
pinned: Boolean(info.committish),
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback for codeberg (not in hosted-git-info)
|
||||
const normalized = url.replace(/^https?:\/\//, "").replace(/@[^/]*$/, "");
|
||||
const codebergHost = "codeberg.org";
|
||||
if (normalized.startsWith(`${codebergHost}/`)) {
|
||||
const ref = url.match(/@([^/]+)$/)?.[1];
|
||||
const repoUrl = ref ? url.slice(0, url.lastIndexOf("@")) : url;
|
||||
// Ensure repo is a valid clone URL
|
||||
const cloneableRepo = repoUrl.startsWith("http") ? repoUrl : `https://${repoUrl}`;
|
||||
return {
|
||||
type: "git",
|
||||
repo: cloneableRepo,
|
||||
host: codebergHost,
|
||||
path: normalized.slice(codebergHost.length + 1).replace(/\.git$/, ""),
|
||||
ref,
|
||||
pinned: Boolean(ref),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue