Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ BETTER_AUTH_SECRET=generate_a_32_char_random_string
BETTER_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_APP_URL=http://localhost:3000

# ──────────────────────────────────────────────
# GitHub Enterprise Server (optional — defaults to github.com)
# ──────────────────────────────────────────────
# NEXT_PUBLIC_GITHUB_WEB_URL=https://gh.zlt.dev
# GITHUB_API_URL=https://gh.zlt.dev/api/v3
# GITHUB_GRAPHQL_URL=https://gh.zlt.dev/api/graphql
# GITHUB_RAW_URL=https://gh.zlt.dev/raw

# ──────────────────────────────────────────────
# Database (required)
# ──────────────────────────────────────────────
Expand Down
20 changes: 20 additions & 0 deletions apps/web/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ const nextConfig: NextConfig = {
},
// reactCompiler: true,
images: {
// GHES private mode requires cookie auth for avatars — the optimizer can't fetch them.
// Disable optimization when targeting GHES so images load directly in the browser.
...(process.env.NEXT_PUBLIC_GITHUB_WEB_URL && { unoptimized: true }),
...(process.env.NODE_ENV === "development" && {
dangerouslyAllowLocalIP: true,
}),
Expand All @@ -53,6 +56,23 @@ const nextConfig: NextConfig = {
{ protocol: "https", hostname: "repository-images.githubusercontent.com" },
{ protocol: "https", hostname: "better-hub.com" },
{ protocol: "https", hostname: "images.better-auth.com" },
// GHES hostname for avatars etc. (reads NEXT_PUBLIC_GITHUB_WEB_URL at build time)
...(process.env.NEXT_PUBLIC_GITHUB_WEB_URL
? [
{
protocol: "https" as const,
hostname: new URL(
process.env
.NEXT_PUBLIC_GITHUB_WEB_URL,
).hostname,
},
// GHES serves avatars from avatars.<hostname>
{
protocol: "https" as const,
hostname: `avatars.${new URL(process.env.NEXT_PUBLIC_GITHUB_WEB_URL).hostname}`,
},
]
: []),
],
},
async headers() {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/(app)/[owner]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { ogImageUrl, ogImages } from "@/lib/og/og-utils";
import { OrgDetailContent } from "@/components/orgs/org-detail-content";
import { UserProfileContent } from "@/components/users/user-profile-content";
import { GITHUB_WEB_URL } from "@/lib/github-config";

export async function generateMetadata({
params,
Expand Down Expand Up @@ -78,7 +79,7 @@ export default async function OwnerPage({ params }: { params: Promise<{ owner: s
avatar_url: orgData.avatar_url,
html_url:
orgData.html_url ??
`https://github.com/${orgData.login}`,
`${GITHUB_WEB_URL}/${orgData.login}`,
description: orgData.description ?? null,
blog: orgData.blog || null,
location: orgData.location || null,
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/(app)/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from "lucide-react";
import { cn } from "@/lib/utils";
import { signOut } from "@/lib/auth-client";
import { GITHUB_WEB_URL } from "@/lib/github-config";

function parseRateLimitFromDigest(message: string) {
// The error message is serialized by Next.js, try to detect rate limit
Expand Down Expand Up @@ -326,7 +327,7 @@ function RateLimitUI({ reset }: { reset: () => void }) {
: "Sign in"}
</button>
<a
href="https://github.com/settings/tokens/new?scopes=repo,read:user,user:email,read:org,notifications&description=Better+GitHub"
href={`${GITHUB_WEB_URL}/settings/tokens/new?scopes=repo,read:user,user:email,read:org,notifications&description=Better+GitHub`}
target="_blank"
rel="noopener noreferrer"
className="text-[11px] text-muted-foreground/50 hover:text-muted-foreground transition-colors"
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/(app)/orgs/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { getUserOrgs } from "@/lib/github";
import { OrgsContent, type OrgListItem } from "@/components/orgs/orgs-content";
import { GITHUB_WEB_URL } from "@/lib/github-config";

export const metadata: Metadata = {
title: "Organizations",
Expand All @@ -14,7 +15,7 @@ export default async function OrgsPage() {
login: org.login,
avatar_url: org.avatar_url,
description: org.description,
html_url: `https://github.com/${org.login}`,
html_url: `${GITHUB_WEB_URL}/${org.login}`,
}));

orgs.sort((a, b) => a.login.localeCompare(b.login));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
extractRepoPermissions,
} from "@/lib/github";
import { parseRefAndPath, formatBytes, getLanguageFromFilename } from "@/lib/github-utils";
import { GITHUB_RAW_URL } from "@/lib/github-config";
import { CodeViewer } from "@/components/repo/code-viewer";
import { MarkdownRenderer } from "@/components/shared/markdown-renderer";
import { MarkdownBlobView } from "@/components/repo/markdown-blob-view";
Expand Down Expand Up @@ -59,7 +60,7 @@ export default async function BlobPage({

// Handle images
if (IMAGE_EXTENSIONS.has(ext)) {
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${path}`;
const rawUrl = `${GITHUB_RAW_URL}/${owner}/${repo}/${ref}/${path}`;
return (
<div className="border border-border p-8 flex items-center justify-center">
{/* eslint-disable-next-line @next/next/no-img-element */}
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/app/(app)/repos/[owner]/[repo]/issues/actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use server";

import { getAuthenticatedUser, getOctokit, invalidateRepoIssuesCache } from "@/lib/github";
import { GITHUB_RAW_URL } from "@/lib/github-config";
import { getErrorMessage } from "@/lib/utils";
import { revalidatePath } from "next/cache";
import { invalidateRepoCache } from "@/lib/repo-data-cache-vc";
Expand Down Expand Up @@ -600,15 +601,15 @@ export async function uploadImage(

return {
success: true,
url: `https://raw.githubusercontent.com/${owner}/${repo}/${targetBranch}/${path}`,
url: `${GITHUB_RAW_URL}/${owner}/${repo}/${targetBranch}/${path}`,
};
} catch (err: any) {
if (err.status === 422) {
// File already exists — construct the URL without a branch re-fetch.
const fallbackBranch = branch ?? "main";
return {
success: true,
url: `https://raw.githubusercontent.com/${owner}/${repo}/${fallbackBranch}/${path}`,
url: `${GITHUB_RAW_URL}/${owner}/${repo}/${fallbackBranch}/${path}`,
};
}
if (err.status === 404 && attempt < 15) {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/(app)/repos/[owner]/[repo]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ import {
import { setCachedRepoTree } from "@/lib/repo-data-cache";
import { waitUntil } from "@vercel/functions";
import { ExternalLink, ShieldAlert, AlertCircle } from "lucide-react";
import { GITHUB_WEB_URL } from "@/lib/github-config";

function RepoErrorPage({ owner, repo, error }: { owner: string; repo: string; error: string }) {
const githubUrl = `https://github.com/${owner}/${repo}`;
const githubUrl = `${GITHUB_WEB_URL}/${owner}/${repo}`;
const isOAuthRestriction = error.includes("OAuth App access restrictions");
const isNotFound = error === "Repository not found";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
Quote,
Search,
} from "lucide-react";
import Image from "next/image";
import Image from "@/components/shared/github-image";
import Link from "next/link";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState, useTransition } from "react";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
invalidateAllPRBundlesForRepo,
getRepoBranches,
} from "@/lib/github";
import { GITHUB_GRAPHQL_URL } from "@/lib/github-config";
import { getErrorMessage } from "@/lib/utils";
import { revalidatePath } from "next/cache";
import { invalidateRepoCache } from "@/lib/repo-data-cache-vc";
Expand Down Expand Up @@ -220,7 +221,7 @@ export async function markPRReadyForReview(owner: string, repo: string, pullNumb
if (!token) return { error: "Not authenticated" };

try {
const idResponse = await fetch("https://api.github.com/graphql", {
const idResponse = await fetch(GITHUB_GRAPHQL_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
Expand All @@ -241,7 +242,7 @@ export async function markPRReadyForReview(owner: string, repo: string, pullNumb
return { error: "Could not find pull request" };
}

const response = await fetch("https://api.github.com/graphql", {
const response = await fetch(GITHUB_GRAPHQL_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
Expand Down Expand Up @@ -469,7 +470,7 @@ export async function resolveReviewThread(
if (!token) return { error: "Not authenticated" };

try {
const response = await fetch("https://api.github.com/graphql", {
const response = await fetch(GITHUB_GRAPHQL_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
Expand Down Expand Up @@ -505,7 +506,7 @@ export async function unresolveReviewThread(
if (!token) return { error: "Not authenticated" };

try {
const response = await fetch("https://api.github.com/graphql", {
const response = await fetch(GITHUB_GRAPHQL_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use server";

import { getOctokit, getGitHubToken } from "@/lib/github";
import { GITHUB_API_URL } from "@/lib/github-config";
import { renderMarkdownToHtml } from "@/components/shared/markdown-renderer";
import { setCachedReadmeHtml } from "@/lib/readme-cache";
import {
Expand Down Expand Up @@ -190,7 +191,7 @@ export async function fetchUsedBy(owner: string, repo: string): Promise<UsedByDa
// Search in package.json dependencies for npm packages
const searchQuery = `"${packageName}" filename:package.json NOT repo:${owner}/${repo}`;
const res = await fetch(
`https://api.github.com/search/code?${new URLSearchParams({
`${GITHUB_API_URL}/search/code?${new URLSearchParams({
q: searchQuery,
per_page: "30",
})}`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type NextRequest, NextResponse } from "next/server";
import { GITHUB_WEB_URL } from "@/lib/github-config";

export async function GET(
_request: NextRequest,
Expand All @@ -7,6 +8,6 @@ export async function GET(
},
) {
const { owner, repo, tag, filename } = await context.params;
const githubUrl = `https://github.com/${owner}/${repo}/releases/download/${encodeURIComponent(tag)}/${filename.join("/")}`;
const githubUrl = `${GITHUB_WEB_URL}/${owner}/${repo}/releases/download/${encodeURIComponent(tag)}/${filename.join("/")}`;
return NextResponse.redirect(githubUrl, { status: 302 });
}
3 changes: 2 additions & 1 deletion apps/web/src/app/(app)/users/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
import { ogImageUrl, ogImages } from "@/lib/og/og-utils";
import { UserProfileContent } from "@/components/users/user-profile-content";
import { ExternalLink, User } from "lucide-react";
import { GITHUB_WEB_URL } from "@/lib/github-config";

function UnknownUserPage({ username }: { username: string }) {
const githubUrl = `https://github.com/${encodeURIComponent(username)}`;
const githubUrl = `${GITHUB_WEB_URL}/${encodeURIComponent(username)}`;

return (
<div className="flex flex-col items-center justify-center py-20 gap-4 text-center">
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/app/api/ai/ghost/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { embedText } from "@/lib/mixedbread";
import { rerankResults } from "@/lib/mixedbread";
import { searchEmbeddings, type ContentType } from "@/lib/embedding-store";
import { toAppUrl } from "@/lib/github-utils";
import { GITHUB_WEB_URL, GITHUB_HOSTNAME } from "@/lib/github-config";
import { getUserSettings } from "@/lib/user-settings-store";
import { checkUsageLimit } from "@/lib/billing/usage-limit";
import { getBillingErrorCode } from "@/lib/billing/config";
Expand Down Expand Up @@ -2729,11 +2730,11 @@ The sandbox has git, node, npm, python, and common dev tools.
repoName = repo;

await sandbox.commands.run(
`git config --global credential.helper store && printf 'protocol=https\\nhost=github.com\\nusername=x-access-token\\npassword=%s\\n' '${githubToken.replace(/'/g, "'\\''")}' | git credential approve`,
`git config --global credential.helper store && printf 'protocol=https\\nhost=${GITHUB_HOSTNAME}\\nusername=x-access-token\\npassword=%s\\n' '${githubToken.replace(/'/g, "'\\''")}' | git credential approve`,
);

await sandbox.commands.run(
`git clone --depth 1 ${branch ? `-b ${branch}` : ""} https://github.com/${owner}/${repo}.git ${repoPath}`,
`git clone --depth 1 ${branch ? `-b ${branch}` : ""} ${GITHUB_WEB_URL}/${owner}/${repo}.git ${repoPath}`,
{ timeoutMs: 300_000 },
);
} catch (e: unknown) {
Expand Down
Loading