/** Deterministic, fail-open facts used to pre-fill the setup interview. */ export const DISCOVERY_TIMEOUT_MS = 5_000; export interface DiscoveryRunResult { exitCode: number; stdout: string; stderr: string; timedOut?: boolean; } export type DiscoveryRunner = ( argv: readonly string[], opts?: { cwd?: string }, ) => Promise; export interface DiscoveredRepoFacts { defaultBranch?: string; labels?: string[]; environments?: string[]; requiredChecks?: string[]; } export interface DiscoveredFacts extends DiscoveredRepoFacts { trackerRepo?: string; routingKey?: string; cloneUrl?: string; artefacts?: string[]; roadmaps?: string[]; evidence?: string[]; skipped?: string[]; } export interface DiscoveryTarget { cwd: string; } function jsonObject(raw: string): Record | undefined { try { const value: unknown = JSON.parse(raw); return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record) : undefined; } catch { return undefined; } } /** Parse `gh repo view --json defaultBranchRef`. */ export function defaultBranchFrom(raw: string): string | undefined { const ref = jsonObject(raw)?.defaultBranchRef; if (typeof ref !== "object" || ref === null || Array.isArray(ref)) return undefined; const name = (ref as Record).name; return typeof name === "string" && name.trim() !== "" ? name.trim() : undefined; } /** Parse `gh label list --json name`. */ export function labelsFrom(raw: string): string[] | undefined { try { const value: unknown = JSON.parse(raw); if (!Array.isArray(value)) return undefined; const names: string[] = []; for (const row of value) { if (typeof row !== "object" || row === null || Array.isArray(row)) return undefined; const name = (row as Record).name; if (typeof name !== "string" || name.trim() === "") return undefined; names.push(name.trim()); } return names; } catch { return undefined; } } /** Parse GitHub's environments REST response. */ export function environmentsFrom(raw: string): string[] | undefined { const rows = jsonObject(raw)?.environments; if (!Array.isArray(rows)) return undefined; const names: string[] = []; for (const row of rows) { if (typeof row !== "object" || row === null || Array.isArray(row)) return undefined; const name = (row as Record).name; if (typeof name !== "string" || name.trim() === "") return undefined; names.push(name.trim()); } return names; } /** Parse branch protection, or the branch's latest check-runs fallback. */ export function requiredChecksFrom(raw: string): string[] | undefined { const parsed = jsonObject(raw); if (parsed === undefined) return undefined; const names: string[] = []; const protection = parsed.required_status_checks; if (protection === null) return []; if (typeof protection === "object" && protection !== null && !Array.isArray(protection)) { const status = protection as Record; if (!Array.isArray(status.contexts) && !Array.isArray(status.checks)) return undefined; if (Array.isArray(status.contexts)) { for (const context of status.contexts) { if (typeof context !== "string" || context.trim() === "") return undefined; names.push(context.trim()); } } if (Array.isArray(status.checks)) { for (const check of status.checks) { if (typeof check !== "object" || check === null || Array.isArray(check)) return undefined; const context = (check as Record).context; if (typeof context !== "string" || context.trim() === "") return undefined; names.push(context.trim()); } } return [...new Set(names)]; } const runs = parsed.check_runs; if (!Array.isArray(runs)) return undefined; for (const run of runs) { if (typeof run !== "object" || run === null || Array.isArray(run)) return undefined; const name = (run as Record).name; if (typeof name !== "string" || name.trim() === "") return undefined; names.push(name.trim()); } return [...new Set(names)]; } /** Parse a package manifest into the npm artefact it publishes. */ export function artefactsFromPackage(raw: string): string[] | undefined { const manifest = jsonObject(raw); const name = manifest?.name; if (manifest?.private === true) return undefined; return typeof name === "string" && name.trim() !== "" ? [name.trim()] : undefined; } /** Parse a recursive Git tree into repository package-manifest paths. */ export function packagePathsFrom(raw: string): string[] | undefined { // A truncated recursive tree is a partial view of a large repository: a // single visible manifest cannot be trusted as "the" artefact, so the whole // field is unavailable rather than guessed from the slice GitHub returned. if (jsonObject(raw)?.truncated === true) return undefined; const tree = jsonObject(raw)?.tree; if (!Array.isArray(tree)) return undefined; const paths: string[] = []; for (const row of tree) { if (typeof row !== "object" || row === null || Array.isArray(row)) return undefined; const path = (row as Record).path; if (typeof path !== "string") return undefined; if ((path === "package.json" || path.endsWith("/package.json")) && !path.includes("node_modules/")) { paths.push(path); } } return paths; } /** Parse open milestones into operator-facing roadmap choices. */ export function milestonesFrom(raw: string): string[] | undefined { try { const value: unknown = JSON.parse(raw); if (!Array.isArray(value)) return undefined; const choices: string[] = []; for (const row of value) { if (typeof row !== "object" || row === null || Array.isArray(row)) return undefined; const milestone = row as Record; if (typeof milestone.title !== "string" || milestone.title.trim() === "") return undefined; choices.push( typeof milestone.html_url === "string" && milestone.html_url.trim() !== "" ? `${milestone.title.trim()} — ${milestone.html_url.trim()}` : milestone.title.trim(), ); } return choices; } catch { return undefined; } } /** Parse `gh project list --format json` into operator-facing roadmap choices. */ export function projectsFrom(raw: string): string[] | undefined { const projects = jsonObject(raw)?.projects; if (!Array.isArray(projects)) return undefined; const choices: string[] = []; for (const row of projects) { if (typeof row !== "object" || row === null || Array.isArray(row)) return undefined; const project = row as Record; if (project.closed === true) continue; if (typeof project.title !== "string" || project.title.trim() === "") return undefined; choices.push( typeof project.url === "string" && project.url.trim() !== "" ? `${project.title.trim()} — ${project.url.trim()}` : project.title.trim(), ); } return choices; } /** Parse a Git remote URL into the `owner/repo` spelling setup accepts. */ export function trackerRepoFrom(raw: string): string | undefined { const remote = raw.trim().replace(/\/$/, "").replace(/\.git$/, ""); if (remote === "") return undefined; let path: string; try { path = new URL(remote).pathname; } catch { const colon = remote.indexOf(":"); if (colon === -1) return undefined; path = remote.slice(colon + 1); } const parts = path.split("/").filter(Boolean); if (parts.length !== 2 || parts.some((part) => !/^[A-Za-z0-9._-]+$/.test(part))) return undefined; return `${parts[0]}/${parts[1]}`; } /** The routing key of a single repository is its clone URL's repository name. */ export function routingKeyFromCloneUrl(raw: string): string | undefined { return trackerRepoFrom(raw)?.split("/")[1]; } /** A bounded subprocess runner. Missing executables and every other fault are data. */ export const runDiscovery: DiscoveryRunner = async (argv, opts = {}) => { try { const proc = Bun.spawn([...argv], { ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }), stdin: "ignore", stdout: "pipe", stderr: "pipe", }); let timedOut = false; const timer = setTimeout(() => { timedOut = true; try { proc.kill("SIGKILL"); } catch { // The child won the race and already exited. } }, DISCOVERY_TIMEOUT_MS); try { const [exitCode, stdout, stderr] = await Promise.all([ proc.exited, new Response(proc.stdout).text(), new Response(proc.stderr).text(), ]); return { exitCode, stdout, stderr, ...(timedOut ? { timedOut: true } : {}) }; } finally { clearTimeout(timer); } } catch (err) { return { exitCode: -1, stdout: "", stderr: err instanceof Error ? err.message : String(err) }; } }; async function attempted( run: DiscoveryRunner, argv: readonly string[], opts?: { cwd?: string }, ): Promise { try { return await run(argv, opts); } catch (err) { return { exitCode: -1, stdout: "", stderr: err instanceof Error ? err.message : String(err) }; } } function failure(result: DiscoveryRunResult): string { if (result.timedOut === true) return `timed out after ${DISCOVERY_TIMEOUT_MS / 1_000}s`; const lines = result.stderr.trim().split("\n").filter(Boolean); return lines.slice(-2).join(" ") || `command exited ${result.exitCode}`; } function parsed( label: string, result: DiscoveryRunResult, parse: (raw: string) => T | undefined, skipped: string[], ): T | undefined { if (result.exitCode !== 0) { skipped.push(`${label}: ${failure(result)}`); return undefined; } const value = parse(result.stdout); if (value === undefined) skipped.push(`${label}: gh returned a malformed reply`); return value; } /** Detailed facts and diagnostics consumed by the wizard's discovery hook. */ async function discoverRepoFactsDetailed( repo: string, run: DiscoveryRunner, ): Promise { const evidence: string[] = []; const skipped: string[] = []; try { const owner = repo.split("/")[0] ?? repo; const [branchReply, labelsReply, environmentsReply, milestonesReply, projectsReply] = await Promise.all([ attempted(run, ["gh", "repo", "view", repo, "--json", "defaultBranchRef"]), attempted(run, ["gh", "label", "list", "--repo", repo, "--limit", "1000", "--json", "name"]), attempted(run, ["gh", "api", `repos/${repo}/environments?per_page=100`]), attempted(run, ["gh", "api", `repos/${repo}/milestones?state=open&per_page=100`]), attempted(run, ["gh", "project", "list", "--owner", owner, "--limit", "100", "--format", "json"]), ]); const defaultBranch = parsed("default branch", branchReply, defaultBranchFrom, skipped); const labels = parsed("labels", labelsReply, labelsFrom, skipped); const environments = parsed("release environments", environmentsReply, environmentsFrom, skipped); const milestones = parsed("open milestones", milestonesReply, milestonesFrom, skipped); const projects = parsed("Projects boards", projectsReply, projectsFrom, skipped); if (defaultBranch !== undefined) evidence.push(`default branch "${defaultBranch}" — gh repo view ${repo}`); if (labels !== undefined) evidence.push(`labels (${labels.join(", ") || "none"}) — gh label list --repo ${repo}`); if (environments !== undefined) { evidence.push(`release environments (${environments.join(", ") || "none"}) — GitHub environments for ${repo}`); } let requiredChecks: string[] | undefined; let artefacts: string[] | undefined; if (defaultBranch !== undefined) { const branch = encodeURIComponent(defaultBranch); const [protectionReply, treeReply] = await Promise.all([ attempted(run, ["gh", "api", `repos/${repo}/branches/${branch}/protection`]), attempted(run, ["gh", "api", `repos/${repo}/git/trees/${branch}?recursive=1`]), ]); requiredChecks = parsed("required checks", protectionReply, requiredChecksFrom, []); let checkEvidence = `branch protection for ${repo}@${defaultBranch}`; if (requiredChecks === undefined) { const runsReply = await attempted(run, ["gh", "api", `repos/${repo}/commits/${branch}/check-runs?per_page=100`]); requiredChecks = parsed("required checks", runsReply, requiredChecksFrom, skipped); checkEvidence = `latest check runs for ${repo}@${defaultBranch}`; } if (requiredChecks !== undefined) { evidence.push(`merge checks (${requiredChecks.join(", ") || "none"}) — ${checkEvidence}`); } let packagePaths: string[] | undefined; if (jsonObject(treeReply.stdout)?.truncated === true) { skipped.push( "release artefacts: the repository's recursive Git tree was truncated, so the visible manifests may be a partial view; the published artefact is left for you to type", ); } else { packagePaths = parsed("release artefacts", treeReply, packagePathsFrom, skipped); } if (packagePaths?.length === 1) { const manifest = await attempted(run, [ "gh", "api", "-H", "Accept: application/vnd.github.raw+json", `repos/${repo}/contents/${packagePaths[0]}`, ]); artefacts = parsed("release artefacts", manifest, artefactsFromPackage, skipped); if (artefacts !== undefined) { evidence.push(`release artefact "${artefacts[0]}" — ${packagePaths[0]} package name`); } } } const roadmaps = projects === undefined && milestones === undefined ? undefined : [...new Set([...(projects ?? []), ...(milestones ?? [])])]; if (roadmaps !== undefined && roadmaps.length > 0) { evidence.push(`roadmap candidates — GitHub Projects and open milestones for ${repo}`); } return { ...(defaultBranch === undefined ? {} : { defaultBranch }), ...(labels === undefined ? {} : { labels }), ...(environments === undefined ? {} : { environments }), ...(requiredChecks === undefined ? {} : { requiredChecks }), ...(artefacts === undefined ? {} : { artefacts }), ...(roadmaps === undefined ? {} : { roadmaps }), evidence, skipped, }; } catch (err) { return { evidence, skipped: [...skipped, `repo discovery: ${err instanceof Error ? err.message : String(err)}`], }; } } /** Discover the four exact repository facts exposed as the module's pure API. */ export async function discoverRepoFacts( repo: string, run: DiscoveryRunner = runDiscovery, ): Promise { const found = await discoverRepoFactsDetailed(repo, run); return { ...(found.defaultBranch === undefined ? {} : { defaultBranch: found.defaultBranch }), ...(found.labels === undefined ? {} : { labels: found.labels }), ...(found.environments === undefined ? {} : { environments: found.environments }), ...(found.requiredChecks === undefined ? {} : { requiredChecks: found.requiredChecks }), }; } /** Discover `owner/repo` from the checkout's origin, or return no value. */ export async function discoverTrackerRepo( cwd: string, run: DiscoveryRunner = runDiscovery, ): Promise { const result = await attempted(run, ["git", "remote", "get-url", "origin"], { cwd }); return result.exitCode === 0 ? trackerRepoFrom(result.stdout) : undefined; } /** Compose checkout and GitHub facts for the wizard's single discovery hook. */ export async function discoverFacts( target: DiscoveryTarget, run: DiscoveryRunner = runDiscovery, ): Promise { const origin = await attempted(run, ["git", "remote", "get-url", "origin"], { cwd: target.cwd }); if (origin.exitCode !== 0) { return { evidence: [], skipped: [`tracker repo: ${failure(origin)}`] }; } const trackerRepo = trackerRepoFrom(origin.stdout); if (trackerRepo === undefined) { return { evidence: [], skipped: ["tracker repo: origin is not an owner/repo Git remote"] }; } const cloneUrl = origin.stdout.trim(); const repo = await discoverRepoFactsDetailed(trackerRepo, run); return { ...repo, trackerRepo, cloneUrl, routingKey: routingKeyFromCloneUrl(cloneUrl), evidence: [`tracker repo "${trackerRepo}" — git remote get-url origin`, ...(repo.evidence ?? [])], }; }