// repo-keys.ts — the SHARED config-key derivation for a repo (KEY: AUTOPILOT-110). // // A repo is keyed in the CONFIG by a CANDIDATE list, most specific // first: the origin slug `owner/repo` (exact) → the slug's bare repo name → the // local basename. Both per-repo maps use it — `shipping.repos` // (AUTO-SHIP-ON-DONE, autopilot.config.json) and `models.repos` // (AUTOPILOT-110/111, ACTUATOR-owned — actuator.config.json, with this repo's // own autopilot.config.json `models` only a fallback; see config.ts) — so one // repo cannot be "acme/thing" to the shipping lane and "thing" to the model map. // // Extracted from shipping.ts at AUTOPILOT-110 so the model resolution // (spawn-model.ts) can match a repo WITHOUT importing shipping.ts: shipping.ts // → config.ts → spawn-model.ts already exists, so a spawn-model → shipping // import would be a cycle. This module imports only ./framework/git.ts (itself // node-builtins-only) — still a leaf, safe for both to depend on. import { basename } from "node:path"; import { git } from "./framework/git.ts"; /** The repo's origin slug `owner/repo` (structural parse of `git remote get-url * origin`), or null. Solely for config-key matching — never a web guess. */ export function originSlug(repo: string): string | null { const url = git(repo, ["remote", "get-url", "origin"]); if (!url) return null; let path = ""; try { const u = new URL(url); if (u.protocol !== "https:" && u.protocol !== "http:" && u.protocol !== "ssh:") return null; path = u.pathname.replace(/^\/+/, ""); } catch { // scp-style git@host:owner/repo.git — the explicit fallback shape const scp = url.replace(/^[^@]+@/, ""); const i = scp.indexOf(":"); if (i <= 0) return null; path = scp.slice(i + 1); } const clean = path.replace(/\.git$/, "").replace(/\/+$/, ""); const parts = clean.split("/").filter(Boolean); if (parts.length < 2) return null; // a bare repo path has no forge slug return parts.slice(-2).join("/"); } /** The config-key candidates for a repo, most specific first: the origin slug * (exact) → the slug's bare repo name → the local basename. */ export function repoPolicyKeys(repo: string): string[] { const keys: string[] = []; const slug = originSlug(repo); if (slug) { keys.push(slug); const bare = slug.split("/").pop(); if (bare && !keys.includes(bare)) keys.push(bare); } const base = basename(repo); if (base && !keys.includes(base)) keys.push(base); return keys; } /** The display/config key suggested for a repo (slug when derivable, else the * repo basename). */ export function suggestedPolicyKey(repo: string): string { return originSlug(repo) ?? basename(repo); }