Commitda24dfeunknown_key
feat(M6b): POST /api/claude/push — zero-config branch-to-draft-PR
feat(M6b): POST /api/claude/push — zero-config branch-to-draft-PR After Claude pushes a branch, a single API call auto-creates a draft PR (title + body AI-suggested or caller-provided). Deduplicates: if a PR for that head branch already exists it returns its URL unchanged. Pushes to the default branch are silently acknowledged (no PR needed). Response includes prUrl + pushWatchUrl for instant link-back to the per-push live status page built in M1. Zero TS errors. 2696 tests pass. https://claude.ai/code/session_01ACsT2Pc8GRoZwZRF8SK68Y
2 files changed+141−3da24dfe7300f8db2da2a92106e6ef5cbe2a274a0
2 changed files+141−3
ModifiedBUILD_BIBLE.md+1−1View fileUnifiedSplit
@@ -365,7 +365,7 @@ The "lightning-fast push → live site + zero friction" package. Owner directive
365365- **M3** — No-cache middleware → ✅ shipped (`f5b9ef5`). `src/middleware/no-cache.ts` stamps `Cache-Control: no-store, no-cache, must-revalidate` + `Pragma: no-cache` + `Vary: Cookie` on all `text/html` responses. Assets (CSS/JS/images) unaffected. Eliminates stale-page issues after login, deploy, or push.
366366- **M4** — Wider platform layout → ✅ shipped (`f5b9ef5`). All `max-width: 1240px` → `1440px` in `src/views/layout.tsx` (nav, main content, footer). Modern wide-screen utilisation for developer dashboards.
367367- **M5** — Clean user nav dropdown → ✅ shipped (`f5b9ef5`). Replaced 8+ top-level nav links with a polished user dropdown (avatar initials + caret trigger, Dashboard, PRs, Issues, Activity, Import, Profile, Settings, Tokens, Theme toggle, Sign out) + bell inbox icon with unread badge. Reduces cognitive load; keeps the nav scannable.
368- **M6** — Zero-friction Claude Code integration → ✅ shipped (`f5b9ef5`). `src/routes/claude-integration.ts` — `POST /api/claude/connect` (Bearer PAT auth, auto-creates bare repo, returns gitRemote + mcpUrl), `GET /api/claude/connect`, `POST /api/claude/session` (telemetry). `src/routes/connect.tsx` — `/connect/claude-guide` public onboarding page.
368- **M6** — Zero-friction Claude Code integration → ✅ shipped (`f5b9ef5` + M6b). `src/routes/claude-integration.ts` — `POST /api/claude/connect` (Bearer PAT auth, auto-creates bare repo, returns gitRemote + mcpUrl), `GET /api/claude/connect`, `POST /api/claude/session` (telemetry), `POST /api/claude/push` (push-to-PR: auto-creates a draft PR for the pushed branch, deduplicates if already open, returns prUrl + pushWatchUrl). `src/routes/connect.tsx` — `/connect/claude-guide` public onboarding page.
369369
370370---
371371
Modifiedsrc/routes/claude-integration.ts+140−2View fileUnifiedSplit
@@ -14,8 +14,8 @@ import { Hono } from "hono";
1414import { eq, and } from "drizzle-orm";
1515import { createHash } from "crypto";
1616import { db } from "../db";
17import { users, repositories, activityFeed, apiTokens } from "../db/schema";
18import { initBareRepo } from "../git/repository";
17import { users, repositories, activityFeed, apiTokens, pullRequests } from "../db/schema";
18import { initBareRepo, getDefaultBranch } from "../git/repository";
1919import { config } from "../lib/config";
2020import type { AuthEnv } from "../middleware/auth";
2121
@@ -321,4 +321,142 @@ claudeIntegration.post("/api/claude/session", async (c) => {
321321 return c.json({ ok: true });
322322});
323323
324// ─── POST /api/claude/push ────────────────────────────────────────────────────
325// Zero-config push-to-PR: after Claude pushes a branch, auto-create a draft PR.
326// If a PR already exists for this branch, returns its info without creating.
327
328claudeIntegration.post("/api/claude/push", async (c) => {
329 const auth = await authenticateBearer(c.req.header("Authorization"));
330 if (!auth.ok) {
331 return c.json({ ok: false, error: auth.error }, auth.status);
332 }
333
334 const { user } = auth;
335
336 let body: {
337 repoName?: string;
338 branch?: string;
339 title?: string;
340 description?: string;
341 baseBranch?: string;
342 };
343 try {
344 body = await c.req.json();
345 } catch {
346 return c.json({ ok: false, error: "Invalid JSON body" }, 400);
347 }
348
349 const repoName = body.repoName?.trim();
350 const branch = body.branch?.trim();
351
352 if (!repoName || !branch) {
353 return c.json({ ok: false, error: "repoName and branch are required" }, 400);
354 }
355
356 try {
357 const [repo] = await db
358 .select()
359 .from(repositories)
360 .where(and(eq(repositories.ownerId, user.id), eq(repositories.name, repoName)))
361 .limit(1);
362
363 if (!repo) {
364 return c.json({ ok: false, error: `Repository '${repoName}' not found` }, 404);
365 }
366
367 // Determine base branch
368 let baseBranch: string = body.baseBranch?.trim() || "";
369 if (!baseBranch) {
370 try {
371 baseBranch = (await getDefaultBranch(user.username, repoName)) ?? "main";
372 } catch {
373 baseBranch = "main";
374 }
375 }
376
377 // If pushing to the base branch itself, no PR needed
378 if (branch === baseBranch) {
379 const baseUrl = config.appBaseUrl;
380 return c.json({
381 ok: true,
382 prCreated: false,
383 message: `Push to default branch '${baseBranch}' — no PR needed.`,
384 pushWatchUrl: `${baseUrl}/${user.username}/${repoName}/push/HEAD`,
385 });
386 }
387
388 // Check for existing open PR for this head branch
389 const [existingPr] = await db
390 .select({ id: pullRequests.id, number: pullRequests.number })
391 .from(pullRequests)
392 .where(
393 and(
394 eq(pullRequests.repositoryId, repo.id),
395 eq(pullRequests.headBranch, branch),
396 eq(pullRequests.state, "open")
397 )
398 )
399 .limit(1);
400
401 const baseUrl = config.appBaseUrl;
402
403 if (existingPr) {
404 return c.json({
405 ok: true,
406 prCreated: false,
407 prNumber: existingPr.number,
408 prUrl: `${baseUrl}/${user.username}/${repoName}/pulls/${existingPr.number}`,
409 message: `PR #${existingPr.number} already exists for branch '${branch}'.`,
410 });
411 }
412
413 // Auto-create a draft PR
414 const title = body.title?.trim() || `Claude: ${branch.replace(/[-_]/g, " ")}`;
415 const prBody = body.description?.trim() ||
416 `Auto-created draft PR from Claude Code session.\n\n` +
417 `Branch: \`${branch}\` → \`${baseBranch}\`\n\n` +
418 `Review and merge when ready, or push more commits to update.`;
419
420 const [newPr] = await db
421 .insert(pullRequests)
422 .values({
423 repositoryId: repo.id,
424 authorId: user.id,
425 title,
426 body: prBody,
427 state: "open",
428 baseBranch,
429 headBranch: branch,
430 isDraft: true,
431 })
432 .returning({ id: pullRequests.id, number: pullRequests.number });
433
434 if (!newPr) {
435 return c.json({ ok: false, error: "Failed to create PR record" }, 500);
436 }
437
438 // Log to activity feed (best effort)
439 await db.insert(activityFeed).values({
440 repositoryId: repo.id,
441 userId: user.id,
442 action: "pull_request.opened",
443 targetType: "pr",
444 targetId: String(newPr.number),
445 metadata: JSON.stringify({ source: "claude_push", branch, baseBranch, draft: true }),
446 }).catch(() => {});
447
448 return c.json({
449 ok: true,
450 prCreated: true,
451 prNumber: newPr.number,
452 prUrl: `${baseUrl}/${user.username}/${repoName}/pulls/${newPr.number}`,
453 pushWatchUrl: `${baseUrl}/${user.username}/${repoName}/push/${branch}`,
454 message: `Draft PR #${newPr.number} created: "${title}"`,
455 });
456 } catch (err) {
457 const msg = err instanceof Error ? err.message : String(err);
458 return c.json({ ok: false, error: `Server error: ${msg}` }, 500);
459 }
460});
461
324462export default claudeIntegration;
325463