feat: add subagents runner config loader
This commit is contained in:
67
src/config.test.ts
Normal file
67
src/config.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { loadSubagentsConfig } from "./config.ts";
|
||||
|
||||
async function makeFixture() {
|
||||
const root = await mkdtemp(join(tmpdir(), "pi-subagents-config-"));
|
||||
const homeDir = join(root, "home");
|
||||
const cwd = join(root, "repo");
|
||||
await mkdir(join(homeDir, ".pi", "agent"), { recursive: true });
|
||||
await mkdir(join(cwd, ".pi"), { recursive: true });
|
||||
return { root, homeDir, cwd };
|
||||
}
|
||||
|
||||
test("loadSubagentsConfig defaults to process when no config files exist", async () => {
|
||||
const { homeDir, cwd } = await makeFixture();
|
||||
|
||||
const config = loadSubagentsConfig(cwd, { homeDir });
|
||||
|
||||
assert.equal(config.runner, "process");
|
||||
assert.equal(config.globalPath, join(homeDir, ".pi", "agent", "subagents.json"));
|
||||
assert.equal(config.projectPath, join(cwd, ".pi", "subagents.json"));
|
||||
});
|
||||
|
||||
test("loadSubagentsConfig uses global config when project config is absent", async () => {
|
||||
const { homeDir, cwd } = await makeFixture();
|
||||
await writeFile(
|
||||
join(homeDir, ".pi", "agent", "subagents.json"),
|
||||
JSON.stringify({ runner: "tmux" }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const config = loadSubagentsConfig(cwd, { homeDir });
|
||||
|
||||
assert.equal(config.runner, "tmux");
|
||||
});
|
||||
|
||||
test("loadSubagentsConfig lets project config override global config", async () => {
|
||||
const { homeDir, cwd } = await makeFixture();
|
||||
await writeFile(
|
||||
join(homeDir, ".pi", "agent", "subagents.json"),
|
||||
JSON.stringify({ runner: "tmux" }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
await writeFile(
|
||||
join(cwd, ".pi", "subagents.json"),
|
||||
JSON.stringify({ runner: "process" }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const config = loadSubagentsConfig(cwd, { homeDir });
|
||||
|
||||
assert.equal(config.runner, "process");
|
||||
});
|
||||
|
||||
test("loadSubagentsConfig throws clear error for invalid runner values", async () => {
|
||||
const { homeDir, cwd } = await makeFixture();
|
||||
const projectPath = join(cwd, ".pi", "subagents.json");
|
||||
await writeFile(projectPath, JSON.stringify({ runner: "fork" }, null, 2), "utf8");
|
||||
|
||||
assert.throws(
|
||||
() => loadSubagentsConfig(cwd, { homeDir }),
|
||||
new RegExp(`Invalid runner .*fork.*${projectPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`),
|
||||
);
|
||||
});
|
||||
48
src/config.ts
Normal file
48
src/config.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
export type RunnerMode = "process" | "tmux";
|
||||
|
||||
export interface SubagentsConfig {
|
||||
runner: RunnerMode;
|
||||
globalPath: string;
|
||||
projectPath: string;
|
||||
}
|
||||
|
||||
export function getSubagentsConfigPaths(cwd: string, homeDir = homedir()) {
|
||||
return {
|
||||
globalPath: join(homeDir, ".pi", "agent", "subagents.json"),
|
||||
projectPath: resolve(cwd, ".pi", "subagents.json"),
|
||||
};
|
||||
}
|
||||
|
||||
function readConfigFile(path: string): { runner?: RunnerMode } | undefined {
|
||||
if (!existsSync(path)) return undefined;
|
||||
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(readFileSync(path, "utf8"));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to parse ${path}: ${message}`);
|
||||
}
|
||||
|
||||
if (parsed.runner !== undefined && parsed.runner !== "process" && parsed.runner !== "tmux") {
|
||||
throw new Error(`Invalid runner ${JSON.stringify(parsed.runner)} in ${path}. Expected "process" or "tmux".`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function loadSubagentsConfig(cwd: string, options: { homeDir?: string } = {}): SubagentsConfig {
|
||||
const { globalPath, projectPath } = getSubagentsConfigPaths(cwd, options.homeDir);
|
||||
const globalConfig = readConfigFile(globalPath) ?? {};
|
||||
const projectConfig = readConfigFile(projectPath) ?? {};
|
||||
|
||||
return {
|
||||
runner: projectConfig.runner ?? globalConfig.runner ?? "process",
|
||||
globalPath,
|
||||
projectPath,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user