feat(KB-200): add pr-create CLI command for task PR creation

- Implement kb task pr-create command to open GitHub PRs from tasks\n- Add draft PR support and title/description customization options\n- Integrate routing in CLI entry point with proper argument parsing\n- Add comprehensive test coverage for the new command\n- Update STANDALONE.md documentation and add changeset for release tracking
This commit is contained in:
gsxdsm
2026-03-31 03:27:11 -07:00
parent d34951a913
commit 3fe8dd3c89
5 changed files with 522 additions and 2 deletions

View File

@@ -5,6 +5,8 @@ import type { PlanningQuestion, PlanningSummary } from "@kb/core";
import { createSession, submitResponse, RateLimitError, SessionNotFoundError, InvalidSessionStateError } from "@kb/dashboard/planning";
import { watchFile, unwatchFile, statSync, existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { GitHubClient } from "@kb/dashboard";
import { isGhAvailable, isGhAuthenticated, getCurrentRepo } from "@kb/core/gh-cli";
const STEP_STATUSES: StepStatus[] = ["pending", "in-progress", "done", "skipped"];
@@ -936,6 +938,128 @@ export async function runTaskSteer(id: string, message?: string) {
console.log();
}
// ── PR Creation ─────────────────────────────────────────────────────────────
export interface PrCreateOptions {
title?: string;
base?: string;
body?: string;
}
export async function runTaskPrCreate(id: string, options: PrCreateOptions = {}) {
const store = await getStore();
// Fetch task and validate it exists
let task;
try {
task = await store.getTask(id);
} catch (err: any) {
if (err.code === "ENOENT") {
console.error(`Error: Task ${id} not found`);
process.exit(1);
}
throw err;
}
// Validate task is in 'in-review' column
if (task.column !== "in-review") {
console.error(`Error: Task must be in 'in-review' column to create a PR (current: ${task.column})`);
process.exit(1);
}
// Check if task already has PR info
if (task.prInfo) {
console.error(`Error: Task already has PR #${task.prInfo.number}: ${task.prInfo.url}`);
process.exit(1);
}
// Determine owner/repo from GITHUB_REPOSITORY env or git remote
let owner: string;
let repo: string;
const envRepo = process.env.GITHUB_REPOSITORY;
if (envRepo) {
const [o, r] = envRepo.split("/");
if (!o || !r) {
console.error("Error: GITHUB_REPOSITORY format is invalid (expected: owner/repo)");
process.exit(1);
}
owner = o;
repo = r;
} else {
const gitRepo = getCurrentRepo(process.cwd());
if (!gitRepo) {
console.error("Error: Could not determine GitHub repository. Set GITHUB_REPOSITORY env var or configure git remote.");
process.exit(1);
}
owner = gitRepo.owner;
repo = gitRepo.repo;
}
// Validate GitHub auth
const hasGhAuth = isGhAvailable() && isGhAuthenticated();
const hasToken = !!process.env.GITHUB_TOKEN;
if (!hasGhAuth && !hasToken) {
console.error("Error: Not authenticated with GitHub. Run 'gh auth login' or set GITHUB_TOKEN.");
process.exit(1);
}
// Build branch name
const branchName = `kb/${id.toLowerCase()}`;
// Build PR title
let title: string;
if (options.title) {
title = options.title;
} else if (task.title) {
title = task.title;
} else {
// Generate from description (first 50 chars, sentence case)
const desc = task.description.trim();
title = desc.charAt(0).toUpperCase() + desc.slice(1, 50);
if (desc.length > 50) {
title += "…";
}
}
// Create PR via GitHubClient
const githubToken = process.env.GITHUB_TOKEN;
const client = new GitHubClient(githubToken);
try {
const prInfo = await client.createPr({
owner,
repo,
title,
body: options.body,
head: branchName,
base: options.base,
});
// Store PR info
await store.updatePrInfo(task.id, prInfo);
await store.logEntry(task.id, "Created PR", `PR #${prInfo.number}: ${prInfo.url}`);
console.log();
console.log(` ✓ Created PR for ${task.id}`);
console.log(` PR #${prInfo.number}: ${prInfo.url}`);
console.log(` Branch: ${branchName}${prInfo.baseBranch}`);
console.log();
} catch (err: any) {
// Handle specific error cases
if (err.message?.includes("already exists")) {
console.error(`Error: A pull request already exists for ${owner}/${repo}:${branchName}`);
process.exit(1);
} else if (err.message?.includes("No commits between")) {
console.error(`Error: No commits between ${options.base || "default base"} and ${branchName}. Push changes before creating PR.`);
process.exit(1);
} else {
console.error(`Error: ${err.message || "Failed to create PR"}`);
process.exit(1);
}
}
}
// ── Planning Mode ───────────────────────────────────────────────────────────
/** Helper to display thinking indicator */