feat: auto-register the surrounding product repo as first base repo (#27)

Running 'bench-kit init' from inside a product repo is the common flow,
so init now detects the git repo containing the invocation cwd (git
rev-parse --show-toplevel + origin remote + HEAD) and replaces the
template's demo-app placeholder in bench.config.yaml with that repo,
editing the YAML document in place so company-zone comments survive.
The detection also lands in instance.json (incl. HEAD as a candidate
pin for the first task). No detection, no origin, or detecting the
instance itself → the placeholder stays. Adds the 'yaml' dependency.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Przemek Smyrdek
2026-08-13 20:55:12 +02:00
committed by GitHub
parent bf798bbe72
commit 7fbe39eb5e
4 changed files with 168 additions and 7 deletions
+3
View File
@@ -8,6 +8,7 @@
"@clack/prompts": "0.9.1",
"cac": "7.0.0",
"proper-lockfile": "4.1.2",
"yaml": "^2.9.0",
},
"devDependencies": {
"@types/bun": "1.3.12",
@@ -159,6 +160,8 @@
"uri-js-replace": ["uri-js-replace@1.0.1", "", {}, "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g=="],
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"yaml-ast-parser": ["yaml-ast-parser@0.0.43", "", {}, "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+2 -1
View File
@@ -32,7 +32,8 @@
"dependencies": {
"@clack/prompts": "0.9.1",
"cac": "7.0.0",
"proper-lockfile": "4.1.2"
"proper-lockfile": "4.1.2",
"yaml": "^2.9.0"
},
"devDependencies": {
"@types/bun": "1.3.12",
+92 -5
View File
@@ -22,8 +22,9 @@ import {
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { basename, join, resolve } from "node:path";
import type { CAC } from "cac";
import { parseDocument } from "yaml";
import { experimentalEnabled, requireExperimental } from "../lib/experimental";
import {
ExitCodes,
@@ -38,12 +39,28 @@ import {
export const TEMPLATE_REPO_URL = "https://github.com/przeprogramowani/10x-bench-kit";
/** The template's placeholder base-repo entry that init may replace. */
export const PLACEHOLDER_BASE_REPO = "demo-app";
/** A git repo detected around the directory init was invoked from. */
export interface DetectedBaseRepo {
/** Repo root directory (used to avoid registering the instance itself). */
rootDir: string;
/** Base-repo name for bench.config.yaml (basename of the repo root). */
name: string;
/** The `origin` remote URL. */
url: string;
/** Current HEAD — a candidate pin for the first task. */
headCommit: string;
}
/** Instance manifest written next to the template's VERSION file. */
export interface InstanceManifest {
templateVersion: string;
templateRef: string;
templateSource: string;
initializedAt: string;
detectedBaseRepo?: DetectedBaseRepo;
}
interface BenchKitFlags extends GlobalFlags {
@@ -63,6 +80,8 @@ export interface BenchKitDeps {
cloneTemplate(ref: string | null, destDir: string): Promise<{ ok: boolean; error: string }>;
/** Runs git with `args` inside `cwd` (fresh `git init` + first commit). */
runGit(args: string[], cwd: string): Promise<{ ok: boolean; error: string }>;
/** Detects the git repo containing `cwd` (null when absent or origin-less). */
detectBaseRepo(cwd: string): Promise<DetectedBaseRepo | null>;
now(): Date;
}
@@ -158,11 +177,25 @@ export async function runBenchKitInit(
mkdirSync(targetDir, { recursive: true });
const copied = materialize(scratch, targetDir, { skipExisting: repair });
// Running init from inside a product repo is the common flow — register
// that repo as the first base repo instead of leaving the placeholder.
let baseRepo: DetectedBaseRepo | null = null;
if (!repair) {
const detected = await deps.detectBaseRepo(process.cwd());
if (detected !== null && resolve(detected.rootDir) !== targetDir) {
if (registerBaseRepo(join(targetDir, "bench.config.yaml"), detected)) {
baseRepo = detected;
verbose(ctx, `registered base repo ${detected.name} (${detected.url})`);
}
}
}
const manifest: InstanceManifest = {
templateVersion,
templateRef: requestedRef ?? "latest",
templateSource: TEMPLATE_REPO_URL,
initializedAt: deps.now().toISOString(),
...(baseRepo === null ? {} : { detectedBaseRepo: baseRepo }),
};
writeFileSync(
join(targetDir, ".bench-kit", "instance.json"),
@@ -181,6 +214,9 @@ export async function runBenchKitInit(
]
: [
`Created a benchmark instance in '${targetDir}' from template ${templateVersion}.`,
baseRepo === null
? "No product repo detected here — add your base repos to bench.config.yaml."
: `Registered '${baseRepo.name}' (${baseRepo.url}) as the first base repo in bench.config.yaml.`,
committed
? "Initialized a fresh git repository with an initial commit."
: "Initialized a fresh git repository (initial commit skipped — commit the files yourself).",
@@ -192,6 +228,7 @@ export async function runBenchKitInit(
templateVersion,
templateRef: manifest.templateRef,
filesCopied: copied,
baseRepo,
gitInitialized: !repair,
committed,
});
@@ -286,6 +323,31 @@ function materialize(
return copied;
}
/**
* Replaces the template's placeholder base-repo entry with the detected
* repo, editing bench.config.yaml in place (comments preserved via yaml
* document editing). Returns false when the config has no placeholder to
* replace — company content is never overwritten on a guess.
*/
export function registerBaseRepo(configPath: string, repo: DetectedBaseRepo): boolean {
if (!existsSync(configPath)) return false;
const doc = parseDocument(readFileSync(configPath, "utf8"));
const firstName = doc.getIn(["base_repos", 0, "name"]);
if (firstName !== PLACEHOLDER_BASE_REPO) return false;
doc.setIn(["base_repos", 0, "name"], repo.name);
doc.setIn(["base_repos", 0, "url"], repo.url);
// The entry is real now — drop the template's per-field placeholder
// comments (file-level comments stay).
const entry = doc.getIn(["base_repos", 0], true);
if (entry && typeof entry === "object" && "items" in entry) {
for (const pair of (entry as { items: { key?: { commentBefore?: string | null } }[] }).items) {
if (pair.key) pair.key.commentBefore = null;
}
}
writeFileSync(configPath, doc.toString());
return true;
}
/** Fresh `git init` + first commit. A failed commit degrades to a warning. */
async function freshGitInit(
ctx: OutputContext,
@@ -321,18 +383,34 @@ async function freshGitInit(
// Default (real) side effects
// ---------------------------------------------------------------------------
function run(cmd: string, args: string[], cwd?: string): Promise<{ ok: boolean; error: string }> {
function run(
cmd: string,
args: string[],
cwd?: string,
): Promise<{ ok: boolean; stdout: string; error: string }> {
return new Promise((resolvePromise) => {
const child = spawn(cmd, args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
const child = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
let stdout = "";
let stderr = "";
child.stdout?.on("data", (chunk: Buffer) => {
stdout += chunk.toString();
});
child.stderr?.on("data", (chunk: Buffer) => {
stderr += chunk.toString();
});
child.on("error", (err) => resolvePromise({ ok: false, error: err.message }));
child.on("close", (code) => resolvePromise({ ok: code === 0, error: stderr }));
child.on("error", (err) => resolvePromise({ ok: false, stdout, error: err.message }));
child.on("close", (code) => resolvePromise({ ok: code === 0, stdout, error: stderr }));
});
}
/** `git -C <cwd> …` returning trimmed stdout, or null on failure. */
async function gitQuery(cwd: string, args: string[]): Promise<string | null> {
const result = await run("git", ["-C", cwd, ...args]);
if (!result.ok) return null;
const value = result.stdout.trim();
return value === "" ? null : value;
}
const defaultDeps: BenchKitDeps = {
async toolAvailable(cmd) {
const result = await run(cmd, ["--version"]);
@@ -347,5 +425,14 @@ const defaultDeps: BenchKitDeps = {
runGit(args, cwd) {
return run("git", args, cwd);
},
async detectBaseRepo(cwd) {
const rootDir = await gitQuery(cwd, ["rev-parse", "--show-toplevel"]);
if (rootDir === null) return null;
const url = await gitQuery(cwd, ["remote", "get-url", "origin"]);
if (url === null) return null;
const headCommit = await gitQuery(cwd, ["rev-parse", "HEAD"]);
if (headCommit === null) return null;
return { rootDir, name: basename(rootDir), url, headCommit };
},
now: () => new Date(),
};
+71 -1
View File
@@ -93,7 +93,19 @@ function buildTemplateFixture(version = "0.1.0"): string {
writeFileSync(join(dir, ".bench-kit", "VERSION"), `${version}\n`);
mkdirSync(join(dir, "tasks", "demo"), { recursive: true });
writeFileSync(join(dir, "tasks", "demo", "prompt.md"), "demo prompt\n");
writeFileSync(join(dir, "bench.config.yaml"), "base_repos: []\n");
writeFileSync(
join(dir, "bench.config.yaml"),
[
"# Konfiguracja instancji benchmarku.",
"base_repos:",
" - name: demo-app",
" # (placeholder)",
" url: git@github.com:example-org/demo-app.git",
"judge:",
" model: anthropic/claude-fable-5",
"",
].join("\n"),
);
return dir;
}
@@ -114,6 +126,7 @@ function fakeDeps(templateDir: string, overrides: Partial<BenchKitDeps> = {}): F
gitCalls.push(args);
return Promise.resolve({ ok: true, error: "" });
},
detectBaseRepo: () => Promise.resolve(null),
now: () => new Date("2026-08-13T12:00:00.000Z"),
...overrides,
};
@@ -154,6 +167,63 @@ describe("10x bench-kit init", () => {
expect(envelope.data.committed).toBe(true);
});
it("registers the surrounding product repo as the first base repo", async () => {
const template = buildTemplateFixture();
const target = join(tempDir("bench-kit-target-"), "instance");
const detected = {
rootDir: "/somewhere/shop-app",
name: "shop-app",
url: "git@github.com:acme/shop-app.git",
headCommit: "a".repeat(40),
};
const { deps } = fakeDeps(template, {
detectBaseRepo: () => Promise.resolve(detected),
});
const result = await captureStreams(() =>
runBenchKitInit(JSON_CTX, target, {}, deps),
);
expect(result.exitCode).toBeUndefined();
const config = readFileSync(join(target, "bench.config.yaml"), "utf8");
expect(config).toContain("name: shop-app");
expect(config).toContain("url: git@github.com:acme/shop-app.git");
expect(config).not.toContain("demo-app");
// Comments survive the in-place edit.
expect(config).toContain("# Konfiguracja instancji benchmarku.");
const manifest = JSON.parse(readFileSync(join(target, ".bench-kit", "instance.json"), "utf8"));
expect(manifest.detectedBaseRepo.name).toBe("shop-app");
expect(manifest.detectedBaseRepo.headCommit).toBe("a".repeat(40));
const envelope = parseEnvelope(result.stdout);
expect(envelope.data.baseRepo.name).toBe("shop-app");
});
it("keeps the placeholder when init runs inside the instance itself", async () => {
const template = buildTemplateFixture();
const target = join(tempDir("bench-kit-target-"), "instance");
const { deps } = fakeDeps(template, {
detectBaseRepo: () =>
Promise.resolve({
rootDir: target,
name: "instance",
url: "git@github.com:acme/instance.git",
headCommit: "b".repeat(40),
}),
});
const result = await captureStreams(() =>
runBenchKitInit(JSON_CTX, target, {}, deps),
);
expect(result.exitCode).toBeUndefined();
const config = readFileSync(join(target, "bench.config.yaml"), "utf8");
expect(config).toContain("name: demo-app");
const envelope = parseEnvelope(result.stdout);
expect(envelope.data.baseRepo).toBeNull();
});
it("refuses a non-empty directory that is not an instance", async () => {
const template = buildTemplateFixture();
const target = tempDir("bench-kit-target-");