feat(config): add interactive config editor and GitLab PR support #4

Merged
Mplan merged 8 commits from v0.1.2 into main 2026-06-12 09:00:29 +08:00
2 changed files with 61 additions and 21 deletions
Showing only changes of commit d0506381f5 - Show all commits
+11 -21
View File
@@ -645,15 +645,6 @@ async function handleCommit(
autoMode: boolean, autoMode: boolean,
dryRun: boolean, dryRun: boolean,
): Promise<"done" | "back"> { ): Promise<"done" | "back"> {
const config = await loadConfig();
if (!config.apiKey) {
console.error(
` ${RED}Error: API key not set. Run ${BOLD}gai config${RESET}${RED} to configure.${RESET}`,
);
process.exit(1);
}
if (!(await isGitRepo())) { if (!(await isGitRepo())) {
console.error(` ${RED}Error: Not a git repository.${RESET}`); console.error(` ${RED}Error: Not a git repository.${RESET}`);
process.exit(1); process.exit(1);
@@ -663,12 +654,7 @@ async function handleCommit(
const unstagedFiles = await getUnstagedFiles(); const unstagedFiles = await getUnstagedFiles();
if (stagedFiles.length === 0 && unstagedFiles.length === 0) { if (stagedFiles.length === 0 && unstagedFiles.length === 0) {
const choice = await selectOne({ console.log(` ${DIM}Nothing to commit. No staged or unstaged changes.${RESET}`);
title: "Nothing to commit",
subtitle: "No staged or unstaged changes.",
items: [{ label: "Back", value: "back" as const }],
});
if (choice === null) process.exit(0);
return "done"; return "done";
} }
@@ -692,15 +678,19 @@ async function handleCommit(
const diff = await getStagedDiff(); const diff = await getStagedDiff();
if (!diff) { if (!diff) {
const choice = await selectOne({ console.log(` ${DIM}Nothing to commit. No staged changes to commit.${RESET}`);
title: "Nothing to commit",
subtitle: "No staged changes to commit.",
items: [{ label: "Back", value: "back" as const }],
});
if (choice === null) process.exit(0);
return "done"; return "done";
} }
const config = await loadConfig();
if (!config.apiKey) {
console.error(
` ${RED}Error: API key not set. Run ${BOLD}gai config${RESET}${RED} to configure.${RESET}`,
);
process.exit(1);
}
const MAX_DIFF_SIZE = 15000; const MAX_DIFF_SIZE = 15000;
const truncatedDiff = const truncatedDiff =
diff.length > MAX_DIFF_SIZE diff.length > MAX_DIFF_SIZE
+50
View File
@@ -0,0 +1,50 @@
import { mkdtempSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { test, expect, describe } from "bun:test";
async function run(command: string[], cwd: string, env: Record<string, string> = {}) {
const proc = Bun.spawn(command, {
cwd,
stdout: "pipe",
stderr: "pipe",
env: {
PATH: process.env.PATH ?? "",
HOME: env.HOME ?? process.env.HOME ?? "",
...env,
},
});
const [exitCode, stdout, stderr] = await Promise.all([
proc.exited,
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
return { exitCode, stdout, stderr };
}
describe("commit command", () => {
test("clean repository exits without requiring API key", async () => {
const repo = mkdtempSync(join(tmpdir(), "gai-clean-repo-"));
const home = mkdtempSync(join(tmpdir(), "gai-empty-home-"));
const init = await run(["git", "init"], repo, { HOME: home });
expect(init.exitCode).toBe(0);
const result = await run(
["bun", "run", join(import.meta.dir, "..", "index.ts"), "commit"],
repo,
{
HOME: home,
GAI_API_KEY: "",
},
);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("Nothing to commit");
expect(result.stdout).toContain("No staged or unstaged changes");
expect(result.stderr).not.toContain("API key not set");
expect(result.stderr).not.toContain("requires a TTY");
});
});