feat(config): add interactive config editor and GitLab PR support #4
@@ -22,7 +22,7 @@ Generate **Conventional Commits** messages and pull request descriptions using A
|
|||||||
- **Conventional Commits** — `feat(scope): description` format by default
|
- **Conventional Commits** — `feat(scope): description` format by default
|
||||||
- **Interactive file selection** — ↑/↓ to navigate, space to select, top-level "Select all"
|
- **Interactive file selection** — ↑/↓ to navigate, space to select, top-level "Select all"
|
||||||
- **Inline editing** — edit AI-generated messages right in the terminal with cursor movement
|
- **Inline editing** — edit AI-generated messages right in the terminal with cursor movement
|
||||||
- **AI-generated PRs** — create GitHub or Gitea pull requests with generated title and body
|
- **AI-generated PRs** — create GitHub, Gitea, or GitLab pull requests with generated title and body
|
||||||
- **OpenAI-compatible API** — works with DeepSeek, OpenAI, Ollama, OpenRouter, and more
|
- **OpenAI-compatible API** — works with DeepSeek, OpenAI, Ollama, OpenRouter, and more
|
||||||
- **Review before commit** — confirm, edit, or abort the generated message
|
- **Review before commit** — confirm, edit, or abort the generated message
|
||||||
- **Bun-native runtime** — built on Bun APIs with no runtime npm dependencies
|
- **Bun-native runtime** — built on Bun APIs with no runtime npm dependencies
|
||||||
@@ -108,6 +108,7 @@ $ gai commit
|
|||||||
|
|
||||||
- GitHub remotes use the `gh` CLI
|
- GitHub remotes use the `gh` CLI
|
||||||
- Gitea remotes use the `tea` CLI
|
- Gitea remotes use the `tea` CLI
|
||||||
|
- GitLab remotes use the `glab` CLI
|
||||||
- Unknown remotes prompt you to choose a platform
|
- Unknown remotes prompt you to choose a platform
|
||||||
|
|
||||||
The command compares your current branch against the default branch, pushes the branch if needed, generates a PR title/body from the branch commits and diff, then asks for confirmation before creating the PR.
|
The command compares your current branch against the default branch, pushes the branch if needed, generates a PR title/body from the branch commits and diff, then asks for confirmation before creating the PR.
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ async function selectPlatform(hostname: string): Promise<Platform | null> {
|
|||||||
items: [
|
items: [
|
||||||
{ label: "GitHub", value: "github" as Platform, description: "Use gh CLI" },
|
{ label: "GitHub", value: "github" as Platform, description: "Use gh CLI" },
|
||||||
{ label: "Gitea", value: "gitea" as Platform, description: "Use tea CLI" },
|
{ label: "Gitea", value: "gitea" as Platform, description: "Use tea CLI" },
|
||||||
|
{ label: "GitLab", value: "gitlab" as Platform, description: "Use glab CLI" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -484,7 +485,8 @@ async function handlePR(draft: boolean): Promise<void> {
|
|||||||
platform = chosen;
|
platform = chosen;
|
||||||
}
|
}
|
||||||
|
|
||||||
const platformLabel = platform === "github" ? "GitHub" : "Gitea";
|
const platformLabel =
|
||||||
|
platform === "github" ? "GitHub" : platform === "gitlab" ? "GitLab" : "Gitea";
|
||||||
console.log(` Using: ${CYAN}${platformLabel}${RESET}`);
|
console.log(` Using: ${CYAN}${platformLabel}${RESET}`);
|
||||||
|
|
||||||
const cliError = checkCLI(platform);
|
const cliError = checkCLI(platform);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type Platform = "github" | "gitea";
|
export type Platform = "github" | "gitea" | "gitlab";
|
||||||
|
|
||||||
export async function getDefaultBranch(): Promise<string> {
|
export async function getDefaultBranch(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
@@ -93,16 +93,23 @@ function parseRemoteHostname(url: string): string | null {
|
|||||||
return hostname || null;
|
return hostname || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function detectPlatformFromHostname(hostname: string | null): Platform | null {
|
||||||
|
if (!hostname) return null;
|
||||||
|
|
||||||
|
if (hostname === "github.com") return "github";
|
||||||
|
if (hostname === "gitlab.com" || hostname.includes("gitlab")) {
|
||||||
|
return "gitlab";
|
||||||
|
}
|
||||||
|
if (hostname.includes("gitea")) return "gitea";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export async function detectPlatform(): Promise<Platform | null> {
|
export async function detectPlatform(): Promise<Platform | null> {
|
||||||
try {
|
try {
|
||||||
const url = await Bun.$`git remote get-url origin`.quiet().text();
|
const url = await Bun.$`git remote get-url origin`.quiet().text();
|
||||||
const hostname = parseRemoteHostname(url);
|
const hostname = parseRemoteHostname(url);
|
||||||
|
|
||||||
if (!hostname) return null;
|
return detectPlatformFromHostname(hostname);
|
||||||
|
|
||||||
if (hostname === "github.com") return "github";
|
|
||||||
if (hostname.includes("gitea")) return "gitea";
|
|
||||||
return null;
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -118,12 +125,16 @@ export async function getRemoteHostname(): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function checkCLI(platform: Platform): string | null {
|
export function checkCLI(platform: Platform): string | null {
|
||||||
const bin = platform === "github" ? "gh" : "tea";
|
const bin =
|
||||||
|
platform === "github" ? "gh" : platform === "gitlab" ? "glab" : "tea";
|
||||||
const path = Bun.which(bin);
|
const path = Bun.which(bin);
|
||||||
if (!path) {
|
if (!path) {
|
||||||
if (platform === "github") {
|
if (platform === "github") {
|
||||||
return "GitHub CLI (gh) not found. Install: brew install gh";
|
return "GitHub CLI (gh) not found. Install: brew install gh";
|
||||||
}
|
}
|
||||||
|
if (platform === "gitlab") {
|
||||||
|
return "GitLab CLI (glab) not found. Install: brew install glab";
|
||||||
|
}
|
||||||
return "Gitea CLI (tea) not found. Install from: https://gitea.com/gitea/tea";
|
return "Gitea CLI (tea) not found. Install from: https://gitea.com/gitea/tea";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -139,6 +150,15 @@ export async function checkAuth(platform: Platform): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (platform === "gitlab") {
|
||||||
|
try {
|
||||||
|
await Bun.$`glab auth status`.quiet();
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return "Not authenticated with GitLab CLI. Run: glab auth login";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await Bun.$`tea logins list`.quiet().text();
|
const result = await Bun.$`tea logins list`.quiet().text();
|
||||||
if (result.trim()) return null;
|
if (result.trim()) return null;
|
||||||
@@ -186,6 +206,37 @@ export async function createPR(
|
|||||||
return match?.[1] ?? stdout.trim();
|
return match?.[1] ?? stdout.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (platform === "gitlab") {
|
||||||
|
const args = [
|
||||||
|
"mr",
|
||||||
|
"create",
|
||||||
|
"--title",
|
||||||
|
title,
|
||||||
|
"--description",
|
||||||
|
body,
|
||||||
|
"--target-branch",
|
||||||
|
base,
|
||||||
|
];
|
||||||
|
if (draft) args.push("--draft");
|
||||||
|
|
||||||
|
const proc = Bun.spawn(["glab", ...args], {
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
});
|
||||||
|
const exitCode = await proc.exited;
|
||||||
|
const stdout = await new Response(proc.stdout).text();
|
||||||
|
const stderr = await new Response(proc.stderr).text();
|
||||||
|
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
stderr.trim() || `glab mr create failed (exit code ${exitCode})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = stdout.match(/(https?:\/\/[^\s]+)/);
|
||||||
|
return match?.[1] ?? stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
"pulls",
|
"pulls",
|
||||||
"create",
|
"create",
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { test, expect, describe } from "bun:test";
|
||||||
|
import { detectPlatformFromHostname } from "../src/pr";
|
||||||
|
|
||||||
|
describe("pr platform detection", () => {
|
||||||
|
test("detects supported hosted platforms", () => {
|
||||||
|
expect(detectPlatformFromHostname("github.com")).toBe("github");
|
||||||
|
expect(detectPlatformFromHostname("gitlab.com")).toBe("gitlab");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects self-hosted platform hostnames", () => {
|
||||||
|
expect(detectPlatformFromHostname("gitlab.example.com")).toBe("gitlab");
|
||||||
|
expect(detectPlatformFromHostname("gitea.example.com")).toBe("gitea");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for unknown hostnames", () => {
|
||||||
|
expect(detectPlatformFromHostname("git.example.com")).toBeNull();
|
||||||
|
expect(detectPlatformFromHostname(null)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user