From c79ffd4303942ee071fa8f6e9514696d9aa2f701 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:20:52 +0800 Subject: [PATCH 01/10] feat: add GitHub account binding for logged-in users Implement GitHub OAuth binding functionality that allows users (e.g., password-authenticated users) to link their GitHub accounts to their existing sessions. Changes: - Add GitHub binding status API (GET /api/user/github) - Add GitHub unbind API (DELETE /api/user/github) - Add OAuth initiation endpoint (GET /api/user/github/bind) - Add OAuth callback handler (GET /api/auth/github/callback) - Add GitHub tab to Settings Dialog with binding UI - Implement popup-based OAuth flow with postMessage communication Technical details: - Use CSRF protection with state parameter stored in httpOnly cookie - Store GitHub credentials in UserIdentity.metadata (token, login, avatar) - Prevent unbinding if GitHub is the only login method - Set isPrimary=false for binding (not primary authentication) - State expires after 10 minutes for security The binding flow uses a popup window to avoid disrupting the main application, with automatic status refresh upon successful binding. --- app/api/auth/github/callback/route.ts | 219 ++++++++++++++++++++++++++ app/api/user/github/bind/route.ts | 62 ++++++++ app/api/user/github/route.ts | 104 ++++++++++++ components/dialog/settings-dialog.tsx | 168 +++++++++++++++++++- 4 files changed, 549 insertions(+), 4 deletions(-) create mode 100644 app/api/auth/github/callback/route.ts create mode 100644 app/api/user/github/bind/route.ts create mode 100644 app/api/user/github/route.ts diff --git a/app/api/auth/github/callback/route.ts b/app/api/auth/github/callback/route.ts new file mode 100644 index 0000000..e01c0d7 --- /dev/null +++ b/app/api/auth/github/callback/route.ts @@ -0,0 +1,219 @@ +import { NextRequest, NextResponse } from 'next/server' + +import { env } from '@/lib/env' +import { logger as baseLogger } from '@/lib/logger' +import { prisma } from '@/lib/db' + +const logger = baseLogger.child({ module: 'api/auth/github/callback' }) + +/** + * GET /api/auth/github/callback + * Handles the OAuth callback from GitHub for account binding + */ +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams + const code = searchParams.get('code') + const state = searchParams.get('state') + + if (!code || !state) { + return NextResponse.json({ error: 'Missing code or state parameter' }, { status: 400 }) + } + + // Verify state parameter + const stateCookie = request.cookies.get('github_oauth_state')?.value + + if (!stateCookie || stateCookie !== state) { + logger.warn('State mismatch in GitHub OAuth callback') + return NextResponse.json({ error: 'Invalid state parameter' }, { status: 400 }) + } + + // Decode state to get userId + let userId: string + try { + const decodedState = Buffer.from(state, 'base64').toString('utf-8') + const [, extractedUserId, timestamp] = decodedState.split('|') + + // Check if state is expired (10 minutes) + const stateAge = Date.now() - parseInt(timestamp, 10) + if (stateAge > 10 * 60 * 1000) { + return NextResponse.json({ error: 'State expired' }, { status: 400 }) + } + + userId = extractedUserId + } catch { + return NextResponse.json({ error: 'Invalid state format' }, { status: 400 }) + } + + // Exchange code for access token + const tokenResponse = await fetch('https://github.com/login/oauth/access_token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + client_id: env.GITHUB_CLIENT_ID, + client_secret: env.GITHUB_CLIENT_SECRET, + code, + }), + }) + + const tokenData = await tokenResponse.json() + + if (!tokenData.access_token) { + logger.error('Failed to get access token from GitHub') + return NextResponse.json({ error: 'Failed to get access token' }, { status: 500 }) + } + + const accessToken = tokenData.access_token + const scope = tokenData.scope || 'repo read:user' + + // Get GitHub user info + const userResponse = await fetch('https://api.github.com/user', { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: 'application/vnd.github.v3+json', + }, + }) + + const githubUser = await userResponse.json() + + if (!githubUser.id) { + logger.error('Failed to get GitHub user info') + return NextResponse.json({ error: 'Failed to get user info' }, { status: 500 }) + } + + const githubUserId = githubUser.id.toString() + const githubLogin = githubUser.login + const githubAvatarUrl = githubUser.avatar_url + + // Check if this GitHub account is already bound to another user + const existingIdentity = await prisma.userIdentity.findUnique({ + where: { + unique_provider_user: { + provider: 'GITHUB', + providerUserId: githubUserId, + }, + }, + }) + + if (existingIdentity && existingIdentity.userId !== userId) { + logger.warn(`GitHub account ${githubLogin} is already bound to another user`) + return createCallbackPage( + false, + 'This GitHub account is already bound to another user account.' + ) + } + + // Upsert GitHub identity + await prisma.userIdentity.upsert({ + where: { + unique_provider_user: { + provider: 'GITHUB', + providerUserId: githubUserId, + }, + }, + update: { + metadata: { + token: accessToken, + scope, + login: githubLogin, + avatar_url: githubAvatarUrl, + }, + }, + create: { + userId, + provider: 'GITHUB', + providerUserId: githubUserId, + metadata: { + token: accessToken, + scope, + login: githubLogin, + avatar_url: githubAvatarUrl, + }, + isPrimary: false, // This is a binding, not primary login + }, + }) + + logger.info(`GitHub account ${githubLogin} bound successfully for user ${userId}`) + + // Return success page that notifies parent window + return createCallbackPage(true, 'GitHub account connected successfully!') + } catch (error) { + logger.error(`Error in GitHub OAuth callback: ${error}`) + return createCallbackPage(false, 'An error occurred during GitHub authentication.') + } +} + +/** + * Create an HTML page that sends a message to the parent window (popup opener) + * and closes itself + */ +function createCallbackPage(success: boolean, message: string): NextResponse { + const html = ` + + + + + GitHub Authentication + + + +
+
${success ? '✅' : '❌'}
+
${message}
+
This window will close automatically...
+
+ + + + ` + + return new NextResponse(html, { + status: 200, + headers: { + 'Content-Type': 'text/html', + }, + }) +} diff --git a/app/api/user/github/bind/route.ts b/app/api/user/github/bind/route.ts new file mode 100644 index 0000000..abead91 --- /dev/null +++ b/app/api/user/github/bind/route.ts @@ -0,0 +1,62 @@ +import { randomBytes } from 'crypto' +import { NextResponse } from 'next/server' + +import { auth } from '@/lib/auth' +import { env } from '@/lib/env' +import { logger as baseLogger } from '@/lib/logger' + +const logger = baseLogger.child({ module: 'api/user/github/bind' }) + +/** + * GET /api/user/github/bind + * Initiates the GitHub OAuth flow for binding + * Redirects to GitHub authorization page with a secure state parameter + */ +export async function GET() { + try { + const session = await auth() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + if (!env.GITHUB_CLIENT_ID || !env.GITHUB_CLIENT_SECRET) { + logger.error('GitHub OAuth is not configured') + return NextResponse.json({ error: 'GitHub OAuth is not configured' }, { status: 500 }) + } + + // Generate a secure random state parameter + const state = randomBytes(32).toString('hex') + + // Store state in a cookie for verification in callback + // Format: state|userId|timestamp + const stateData = `${state}|${session.user.id}|${Date.now()}` + const encodedState = Buffer.from(stateData).toString('base64') + + // Build GitHub OAuth URL + const githubAuthUrl = new URL('https://github.com/login/oauth/authorize') + githubAuthUrl.searchParams.set('client_id', env.GITHUB_CLIENT_ID) + githubAuthUrl.searchParams.set('redirect_uri', `${process.env.NEXTAUTH_URL}/api/auth/github/callback`) + githubAuthUrl.searchParams.set('scope', 'repo read:user') + githubAuthUrl.searchParams.set('state', encodedState) + + logger.info(`GitHub OAuth bind initiated for user ${session.user.id}`) + + // Create response with redirect + const response = NextResponse.redirect(githubAuthUrl.toString()) + + // Set state cookie (expires in 10 minutes) + response.cookies.set('github_oauth_state', encodedState, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 600, // 10 minutes + path: '/', + }) + + return response + } catch (error) { + logger.error(`Error initiating GitHub OAuth bind: ${error}`) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/app/api/user/github/route.ts b/app/api/user/github/route.ts new file mode 100644 index 0000000..0f37212 --- /dev/null +++ b/app/api/user/github/route.ts @@ -0,0 +1,104 @@ +import { NextResponse } from 'next/server' + +import { auth } from '@/lib/auth' +import { prisma } from '@/lib/db' +import { logger as baseLogger } from '@/lib/logger' + +const logger = baseLogger.child({ module: 'api/user/github' }) + +/** + * GET /api/user/github + * Returns the GitHub binding status for the current user + */ +export async function GET() { + try { + const session = await auth() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Find GitHub identity for this user + const githubIdentity = await prisma.userIdentity.findFirst({ + where: { + userId: session.user.id, + provider: 'GITHUB', + }, + }) + + if (!githubIdentity) { + return NextResponse.json({ connected: false }) + } + + // Extract GitHub info from metadata + const metadata = githubIdentity.metadata as { + login?: string + avatar_url?: string + } + + return NextResponse.json({ + connected: true, + login: metadata.login, + avatar_url: metadata.avatar_url, + }) + } catch (error) { + logger.error(`Error fetching GitHub status: ${error}`) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +/** + * DELETE /api/user/github + * Unbinds the GitHub account from the current user + */ +export async function DELETE() { + try { + const session = await auth() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Find and delete GitHub identity + const githubIdentity = await prisma.userIdentity.findFirst({ + where: { + userId: session.user.id, + provider: 'GITHUB', + }, + }) + + if (!githubIdentity) { + return NextResponse.json({ error: 'No GitHub account connected' }, { status: 404 }) + } + + // Check if this is the primary (and only) identity + const identityCount = await prisma.userIdentity.count({ + where: { + userId: session.user.id, + }, + }) + + if (identityCount === 1 && githubIdentity.isPrimary) { + return NextResponse.json( + { + error: 'Cannot unbind the only login method. Please add another login method first.', + }, + { status: 400 } + ) + } + + // Delete the GitHub identity + await prisma.userIdentity.delete({ + where: { + id: githubIdentity.id, + }, + }) + + logger.info(`GitHub account unbound for user ${session.user.id}`) + + return NextResponse.json({ success: true }) + } catch (error) { + logger.error(`Error unbinding GitHub account: ${error}`) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/components/dialog/settings-dialog.tsx b/components/dialog/settings-dialog.tsx index 568c355..58abadd 100644 --- a/components/dialog/settings-dialog.tsx +++ b/components/dialog/settings-dialog.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { Code, Database, Save, Terminal } from 'lucide-react'; +import { Code, Database, Github, Save, Terminal } from 'lucide-react'; import { toast } from 'sonner'; import { @@ -26,7 +26,7 @@ import { useSealos } from '@/provider/sealos'; interface SettingsDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - defaultTab?: 'system-prompt' | 'kubeconfig' | 'anthropic'; + defaultTab?: 'system-prompt' | 'kubeconfig' | 'anthropic' | 'github'; } const DEFAULT_SYSTEM_PROMPT = `You are an AI full-stack developer working in a Next.js environment. @@ -57,7 +57,13 @@ const DEFAULT_SYSTEM_PROMPT = `You are an AI full-stack developer working in a N - Optimize for performance and SEO - Follow modern React patterns and best practices`; -type TabType = 'system-prompt' | 'kubeconfig' | 'anthropic'; +type TabType = 'system-prompt' | 'kubeconfig' | 'anthropic' | 'github'; + +interface GitHubStatus { + connected: boolean; + login?: string; + avatar_url?: string; +} export default function SettingsDialog({ open, @@ -86,6 +92,11 @@ export default function SettingsDialog({ const [isAnthropicLoading, setIsAnthropicLoading] = useState(false); const [isAnthropicInitialLoading, setIsAnthropicInitialLoading] = useState(true); + // GitHub state + const [githubStatus, setGithubStatus] = useState({ connected: false }); + const [isGithubLoading, setIsGithubLoading] = useState(false); + const [isGithubInitialLoading, setIsGithubInitialLoading] = useState(true); + // Confirmation dialog state const [showSystemPromptConfirm, setShowSystemPromptConfirm] = useState(false); const [showSystemPromptResetConfirm, setShowSystemPromptResetConfirm] = useState(false); @@ -99,6 +110,7 @@ export default function SettingsDialog({ loadKubeconfig(); } loadAnthropicConfig(); + loadGithubStatus(); } }, [open, isSealos]); @@ -160,6 +172,18 @@ export default function SettingsDialog({ } }; + const loadGithubStatus = async () => { + try { + const data = await fetchClient.GET('/api/user/github'); + setGithubStatus(data); + } catch (error) { + console.error('Failed to load GitHub status:', error); + setGithubStatus({ connected: false }); + } finally { + setIsGithubInitialLoading(false); + } + }; + const handleSaveSystemPrompt = () => { setShowSystemPromptConfirm(true); }; @@ -253,6 +277,66 @@ export default function SettingsDialog({ toast.success('Reset to default system prompt'); }; + const handleConnectGithub = () => { + setIsGithubLoading(true); + + // Open popup window for GitHub OAuth + const width = 600; + const height = 700; + const left = window.screen.width / 2 - width / 2; + const top = window.screen.height / 2 - height / 2; + + const popup = window.open( + '/api/user/github/bind', + 'github-oauth', + `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes` + ); + + if (!popup) { + toast.error('Failed to open popup window. Please allow popups for this site.'); + setIsGithubLoading(false); + return; + } + + // Listen for message from popup + const handleMessage = (event: MessageEvent) => { + if (event.origin !== window.location.origin) return; + if (event.data.type !== 'github-oauth-callback') return; + + if (event.data.success) { + toast.success('GitHub account connected successfully!'); + loadGithubStatus(); + } else { + toast.error(event.data.message || 'Failed to connect GitHub account'); + } + + setIsGithubLoading(false); + window.removeEventListener('message', handleMessage); + }; + + window.addEventListener('message', handleMessage); + + // Fallback: stop loading after timeout + setTimeout(() => { + setIsGithubLoading(false); + window.removeEventListener('message', handleMessage); + }, 60000); // 1 minute timeout + }; + + const handleDisconnectGithub = async () => { + setIsGithubLoading(true); + try { + await fetchClient.DELETE('/api/user/github'); + toast.success('GitHub account disconnected successfully'); + setGithubStatus({ connected: false }); + } catch (error: unknown) { + console.error('Failed to disconnect GitHub:', error); + toast.error(error instanceof Error ? error.message : 'Failed to disconnect GitHub account'); + } finally { + setIsGithubLoading(false); + } + }; + return ( Anthropic + + + GitHub +
@@ -503,6 +594,75 @@ export default function SettingsDialog({
+ + {/* GitHub Tab */} + +
+
+ +

+ Connect your GitHub account to enable repository access and code management features. +

+
+ + {isGithubInitialLoading ? ( +
+
Loading...
+
+ ) : githubStatus.connected ? ( + // Connected state +
+
+ {githubStatus.avatar_url && ( + GitHub Avatar + )} +
+
+ + {githubStatus.login} + + ● Connected +
+

+ Your GitHub account is connected and ready to use. +

+
+
+ + +
+ ) : ( + // Not connected state +
+
+

+ No GitHub account connected. Connect your GitHub account to access repositories and enable version control features. +

+
+ + +
+ )} +
+
From 63ea8754f8b846991d0871f4386d8c57b00c36db Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:17:25 +0800 Subject: [PATCH 02/10] add repo connection status to status bar --- components/layout/status-bar.tsx | 9 ++++++++- lib/services/repoService.ts | 0 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 lib/services/repoService.ts diff --git a/components/layout/status-bar.tsx b/components/layout/status-bar.tsx index 3a81432..a3f1abd 100644 --- a/components/layout/status-bar.tsx +++ b/components/layout/status-bar.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Prisma } from '@prisma/client'; -import { Box, Database } from 'lucide-react'; +import { Box, Database, FolderGit, RefreshCw } from 'lucide-react'; type ProjectWithRelations = Prisma.ProjectGetPayload<{ include: { @@ -25,6 +25,13 @@ export function StatusBar({ project }: StatusBarProps) { return (
+
+ + {project?.githubRepo || 'Initialize GitHub Repo'} +
+
+ +
diff --git a/lib/services/repoService.ts b/lib/services/repoService.ts new file mode 100644 index 0000000..e69de29 From 8bf5c7ee61b6e4c0aaa9aa766cd0788d4fd895d1 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Thu, 11 Dec 2025 21:49:54 +0800 Subject: [PATCH 03/10] fix: ensure changes are pushed to remote and improve repo status UI - Integrate pushToGithub into initializeRepo and commitChanges workflows to ensure code syncs to remote. - Add router.refresh() in RepoStatusIndicator to update UI state immediately after initialization. - Refactor RepoStatusIndicator to use semantic button elements for accessibility. - Enhance UI with better loading states and adjusted icon sizes. --- components/layout/repo-status-indicator.tsx | 109 ++++++++ components/layout/status-bar.tsx | 24 +- lib/services/repoService.ts | 281 ++++++++++++++++++++ lib/util/ttyd-exec.ts | 4 +- 4 files changed, 405 insertions(+), 13 deletions(-) create mode 100644 components/layout/repo-status-indicator.tsx diff --git a/components/layout/repo-status-indicator.tsx b/components/layout/repo-status-indicator.tsx new file mode 100644 index 0000000..21819d4 --- /dev/null +++ b/components/layout/repo-status-indicator.tsx @@ -0,0 +1,109 @@ +'use client' + +import { useState } from 'react' +import { Project } from '@prisma/client' +import { Github, Loader2, RefreshCw } from 'lucide-react' +import { useRouter } from 'next/navigation' +import { toast } from 'sonner' + +import { commitChanges,initializeRepo } from '@/lib/services/repoService' + +interface RepoStatusIndicatorProps { + project: Pick +} + +export function RepoStatusIndicator({ project }: RepoStatusIndicatorProps) { + const router = useRouter() + const [isInitializing, setIsInitializing] = useState(false) + const [isCommitting, setIsCommitting] = useState(false) + + // Create a new repository on GitHub + const handleInitialize = async () => { + if (project.githubRepo || isInitializing) return + + setIsInitializing(true) + try { + const result = await initializeRepo(project.id) + if (result.success) { + toast.success(result.message) + router.refresh() + } else { + toast.error(result.message) + } + } catch (_error) { + toast.error('An unexpected error occurred') + } finally { + setIsInitializing(false) + } + } + + // Commit changes to the repository and push to GitHub + const handleCommit = async (e: React.MouseEvent) => { + e.stopPropagation() // Prevent triggering other clicks if needed + if (isCommitting) return + + setIsCommitting(true) + try { + const result = await commitChanges(project.id) + if (result.success) { + toast.success(result.message) + } else { + toast.error(result.message) + } + } catch (_error) { + toast.error('Failed to commit changes') + } finally { + setIsCommitting(false) + } + } + + const isLoading = isInitializing || isCommitting + + return ( +
+
+
+ {(!project.githubRepo && isInitializing) ? ( + + ) : ( + + )} +
+ + {project.githubRepo ? ( + + {project.name} + + ) : ( + + )} +
+ + {project.githubRepo && ( + + )} +
+ ) +} diff --git a/components/layout/status-bar.tsx b/components/layout/status-bar.tsx index a3f1abd..29f1767 100644 --- a/components/layout/status-bar.tsx +++ b/components/layout/status-bar.tsx @@ -1,6 +1,9 @@ import React from 'react'; import { Prisma } from '@prisma/client'; -import { Box, Database, FolderGit, RefreshCw } from 'lucide-react'; +import { Box, Database } from 'lucide-react'; + +import { RepoStatusIndicator } from '@/components/layout/repo-status-indicator'; +import { getStatusIconColor } from '@/lib/util/status-colors'; type ProjectWithRelations = Prisma.ProjectGetPayload<{ include: { @@ -11,27 +14,24 @@ type ProjectWithRelations = Prisma.ProjectGetPayload<{ }>; interface StatusBarProps { - project?: ProjectWithRelations; + project: ProjectWithRelations; } -import { getStatusIconColor } from '@/lib/util/status-colors'; + export function StatusBar({ project }: StatusBarProps) { - const database = project?.databases?.[0]; + const database = project.databases?.[0]; const dbStatus = database?.status || 'CREATING'; - const sandbox = project?.sandboxes?.[0]; + const sandbox = project.sandboxes?.[0]; const sbStatus = sandbox?.status || 'CREATING'; return (
-
- - {project?.githubRepo || 'Initialize GitHub Repo'} -
-
- -
+ +
diff --git a/lib/services/repoService.ts b/lib/services/repoService.ts index e69de29..01445c3 100644 --- a/lib/services/repoService.ts +++ b/lib/services/repoService.ts @@ -0,0 +1,281 @@ +'use server' + +import { auth } from '@/lib/auth' +import { prisma } from '@/lib/db' +import { execCommand, TtydExecError } from '@/lib/util/ttyd-exec' + +export type RepoInitResult = { + success: boolean + message: string +} + +/** + * Helper to get project TTYD context and verify ownership. + * This ensures the project belongs to the requesting user before proceeding. + */ +async function getTtydContext(projectId: string, userId: string) { + // security measure + const project = await prisma.project.findFirst({ + where: { + id: projectId, + userId: userId, + }, + include: { + sandboxes: true, + environments: true, + }, + }) + + if (!project) { + throw new Error('Project not found') + } + + const sandbox = project.sandboxes[0] + if (!sandbox) { + throw new Error('Sandbox not found') + } + + const ttydAccessToken = project.environments.find( + (env) => env.key === 'TTYD_ACCESS_TOKEN' + )?.value + + if (!sandbox.ttydUrl || !ttydAccessToken) { + throw new Error('Sandbox configuration missing') + } + + // Parse the ttydUrl to get base URL (without query params) + const ttydBaseUrl = new URL(sandbox.ttydUrl) + ttydBaseUrl.search = '' // Remove query params + const baseUrl = ttydBaseUrl.toString().replace(/\/$/, '') + + return { baseUrl, accessToken: ttydAccessToken, project } +} + + +/** + * Initialize a git repository in the project's sandbox + * @param projectId - The ID of the project + */ +export async function initializeRepo(projectId: string): Promise { + const session = await auth() + + if (!session) { + return { success: false, message: 'Unauthorized' } + } + + try { + const { baseUrl, accessToken, project } = await getTtydContext(projectId, session.user.id) + + // Create GitHub repo first + const repoResult = await createGithubRepo(project.name) + if (!repoResult.success) { + return { success: false, message: repoResult.message } + } + + // Save repo URL to database + await prisma.project.update({ + where: { id: projectId }, + data: { githubRepo: repoResult.repoUrl }, + }) + + await runInitCommand(baseUrl, accessToken) + + // Push the initial code to GitHub + const pushResult = await pushToGithub(projectId) + if (!pushResult.success) { + return { success: true, message: `Initialized locally but failed to push: ${pushResult.message}` } + } + + return { success: true, message: 'Repository initialized and pushed successfully' } + } catch (error) { + console.error('Failed to initialize repo:', error) + const errorMessage = error instanceof TtydExecError ? error.message : 'Unknown error' + return { success: false, message: `Failed to initialize: ${errorMessage}` } + } +} + +async function runInitCommand(baseUrl: string, accessToken: string) { + return execCommand( + baseUrl, + accessToken, + 'git init -b main && git add . && claude -p "commit all staged changes with a descriptive message" --dangerously-skip-permissions', + 300000 + ) + + +} + +export type CreateRepoResult = { + success: boolean + message: string + repoUrl?: string + cloneUrl?: string +} + +/** + * Create a new GitHub repository for the user + * @param repoName - The name of the new repository + */ +export async function createGithubRepo(repoName: string): Promise { + const session = await auth() + + if (!session) { + return { success: false, message: 'Unauthorized' } + } + + try { + // Find UserIdentity for GitHub to get the token + const identity = await prisma.userIdentity.findFirst({ + where: { + userId: session.user.id, + provider: 'GITHUB', + }, + }) + + if (!identity) { + return { success: false, message: 'GitHub identity not found. Please link your GitHub account.' } + } + + const metadata = identity.metadata as { token?: string } + const token = metadata?.token + + if (!token) { + return { success: false, message: 'GitHub token not found in identity metadata.' } + } + + // Call GitHub API to create repository + const response = await fetch('https://api.github.com/user/repos', { + method: 'POST', + headers: { + Authorization: `token ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/vnd.github.v3+json', + }, + body: JSON.stringify({ + name: repoName, + private: true, // Default to private + description: 'Created by Fulling, Powered by Sealos', + auto_init: false, // Don't create README/LICENSE, we'll push existing code + }), + }) + + if (!response.ok) { + const errorData = await response.json() + return { + success: false, + message: `GitHub API Error: ${errorData.message || response.statusText}` + } + } + + const repoData = await response.json() + + return { + success: true, + message: 'Repository created successfully', + repoUrl: repoData.html_url, + cloneUrl: repoData.clone_url, + } + } catch (error) { + console.error('Failed to create GitHub repo:', error) + return { success: false, message: `Failed to create repository: ${error instanceof Error ? error.message : String(error)}` } + } +} + +/** + * Commit all changes in the project's sandbox + * @param projectId - The ID of the project + */ +export async function commitChanges(projectId: string): Promise { + const session = await auth() + + if (!session) { + return { success: false, message: 'Unauthorized' } + } + + try { + const { baseUrl, accessToken } = await getTtydContext(projectId, session.user.id) + + await execCommand( + baseUrl, + accessToken, + 'git add . && claude -p "commit all staged changes with a descriptive message" --dangerously-skip-permissions', + ) + + // Push changes to GitHub + const pushResult = await pushToGithub(projectId) + if (!pushResult.success) { + return { success: true, message: `Committed locally but failed to push: ${pushResult.message}` } + } + + return { success: true, message: 'Changes committed and pushed successfully' } + } catch (error) { + console.error('Failed to commit changes:', error) + const errorMessage = error instanceof TtydExecError ? error.message : 'Unknown error' + return { success: false, message: `Failed to commit: ${errorMessage}` } + } +} + + + +/** + * Push local commits to GitHub + * @param projectId - The ID of the project + */ +export async function pushToGithub(projectId: string): Promise { + const session = await auth() + + if (!session) { + return { success: false, message: 'Unauthorized' } + } + + try { + const { baseUrl, accessToken, project } = await getTtydContext(projectId, session.user.id) + + if (!project.githubRepo) { + return { success: false, message: 'No GitHub repository linked to this project' } + } + + // Get GitHub token + const identity = await prisma.userIdentity.findFirst({ + where: { + userId: session.user.id, + provider: 'GITHUB', + }, + }) + + // Type checking for metadata token + const metadata = identity?.metadata as { token?: string } | undefined + const githubToken = metadata?.token + + if (!githubToken) { + return { success: false, message: 'GitHub token not found' } + } + + // Extract owner/repo from URL (e.g., https://github.com/owner/repo) + // We want to construct: https://oauth2:token@github.com/owner/repo.git + let repoUrlStr = project.githubRepo + if (!repoUrlStr.endsWith('.git')) { + repoUrlStr += '.git' + } + + // Remove protocol to insert auth + const urlNoProtocol = repoUrlStr.replace(/^https?:\/\//, '') + const authUrl = `https://oauth2:${githubToken}@${urlNoProtocol}` + + // Configure remote and push + // We use 'git remote set-url' if origin exists, or 'git remote add' if it doesn't + const command = ` + (git remote get-url origin > /dev/null 2>&1 && git remote set-url origin ${authUrl}) || git remote add origin ${authUrl} && + git branch -M main && + git push -u origin main + `.replace(/\n/g, ' ').trim() + + await execCommand(baseUrl, accessToken, command, 300000) + + return { success: true, message: 'Code pushed to GitHub successfully' } + } catch (error) { + console.error('Failed to push to GitHub:', error) + const errorMessage = error instanceof TtydExecError ? error.message : 'Unknown error' + return { success: false, message: `Failed to push: ${errorMessage}` } + } +} diff --git a/lib/util/ttyd-exec.ts b/lib/util/ttyd-exec.ts index 5afc636..c37a40c 100644 --- a/lib/util/ttyd-exec.ts +++ b/lib/util/ttyd-exec.ts @@ -525,12 +525,14 @@ export async function executeTtydCommand(options: TtydExecOptions): Promise { const result = await executeTtydCommand({ ttydUrl, accessToken, command, + timeoutMs, }) if (result.timedOut) { From afa61631d71c473c67fdc2b1995e6f92234442a8 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:44:50 +0800 Subject: [PATCH 04/10] fix lint issue --- app/api/auth/github/callback/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/auth/github/callback/route.ts b/app/api/auth/github/callback/route.ts index e01c0d7..58277df 100644 --- a/app/api/auth/github/callback/route.ts +++ b/app/api/auth/github/callback/route.ts @@ -1,8 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/db' import { env } from '@/lib/env' import { logger as baseLogger } from '@/lib/logger' -import { prisma } from '@/lib/db' const logger = baseLogger.child({ module: 'api/auth/github/callback' }) From 217bb2ae61ed2848c417bd955759e4b8e88c8b1c Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:22:12 +0800 Subject: [PATCH 05/10] fix(security): sanitize repo URL and patch command injection in repoService --- lib/services/repoService.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/services/repoService.ts b/lib/services/repoService.ts index 01445c3..d02b0cc 100644 --- a/lib/services/repoService.ts +++ b/lib/services/repoService.ts @@ -251,6 +251,13 @@ export async function pushToGithub(projectId: string): Promise { return { success: false, message: 'GitHub token not found' } } + // Validate GitHub URL format to prevent injection attacks + // Allow standard GitHub URLs: https://github.com/username/repo or https://github.com/username/repo.git + const githubUrlPattern = /^https:\/\/github\.com\/[a-zA-Z0-9-]+\/[a-zA-Z0-9-._]+(\.git)?$/ + if (!githubUrlPattern.test(project.githubRepo)) { + return { success: false, message: 'Invalid GitHub repository URL' } + } + // Extract owner/repo from URL (e.g., https://github.com/owner/repo) // We want to construct: https://oauth2:token@github.com/owner/repo.git let repoUrlStr = project.githubRepo @@ -264,8 +271,9 @@ export async function pushToGithub(projectId: string): Promise { // Configure remote and push // We use 'git remote set-url' if origin exists, or 'git remote add' if it doesn't + // SECURITY: Use single quotes around URL to prevent command injection const command = ` - (git remote get-url origin > /dev/null 2>&1 && git remote set-url origin ${authUrl}) || git remote add origin ${authUrl} && + (git remote get-url origin > /dev/null 2>&1 && git remote set-url origin '${authUrl}') || git remote add origin '${authUrl}' && git branch -M main && git push -u origin main `.replace(/\n/g, ' ').trim() From 106a1dc5c8014347fcd02e17c4428af8b218b7e2 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:12:33 +0800 Subject: [PATCH 06/10] feat(repo): auto-prompt settings dialog when GitHub is not bound Update repoService to propagate 'GITHUB_NOT_BOUND' error code when identity is missing. Update RepoStatusIndicator to catch this error and trigger the local SettingsDialog, streamlining the authentication flow. --- components/layout/repo-status-indicator.tsx | 98 +++++++++++++-------- lib/services/repoService.ts | 14 ++- 2 files changed, 69 insertions(+), 43 deletions(-) diff --git a/components/layout/repo-status-indicator.tsx b/components/layout/repo-status-indicator.tsx index 21819d4..d751713 100644 --- a/components/layout/repo-status-indicator.tsx +++ b/components/layout/repo-status-indicator.tsx @@ -6,7 +6,8 @@ import { Github, Loader2, RefreshCw } from 'lucide-react' import { useRouter } from 'next/navigation' import { toast } from 'sonner' -import { commitChanges,initializeRepo } from '@/lib/services/repoService' +import SettingsDialog from '@/components/dialog/settings-dialog' +import { commitChanges, initializeRepo } from '@/lib/services/repoService' interface RepoStatusIndicatorProps { project: Pick @@ -16,6 +17,7 @@ export function RepoStatusIndicator({ project }: RepoStatusIndicatorProps) { const router = useRouter() const [isInitializing, setIsInitializing] = useState(false) const [isCommitting, setIsCommitting] = useState(false) + const [showSettings, setShowSettings] = useState(false) // Create a new repository on GitHub const handleInitialize = async () => { @@ -28,7 +30,12 @@ export function RepoStatusIndicator({ project }: RepoStatusIndicatorProps) { toast.success(result.message) router.refresh() } else { - toast.error(result.message) + if (result.code === 'GITHUB_NOT_BOUND') { + toast.error('Please connect your GitHub account first') + setShowSettings(true) + } else { + toast.error(result.message) + } } } catch (_error) { toast.error('An unexpected error occurred') @@ -48,7 +55,12 @@ export function RepoStatusIndicator({ project }: RepoStatusIndicatorProps) { if (result.success) { toast.success(result.message) } else { - toast.error(result.message) + if (result.code === 'GITHUB_NOT_BOUND') { + toast.error('Please connect your GitHub account first') + setShowSettings(true) + } else { + toast.error(result.message) + } } } catch (_error) { toast.error('Failed to commit changes') @@ -60,50 +72,58 @@ export function RepoStatusIndicator({ project }: RepoStatusIndicatorProps) { const isLoading = isInitializing || isCommitting return ( -
-
-
- {(!project.githubRepo && isInitializing) ? ( - + <> +
+
+
+ {(!project.githubRepo && isInitializing) ? ( + + ) : ( + + )} +
+ + {project.githubRepo ? ( + + {project.name} + ) : ( - + )}
- - {project.githubRepo ? ( - - {project.name} - - ) : ( + + {project.githubRepo && ( )}
- {project.githubRepo && ( - - )} -
+ + ) } diff --git a/lib/services/repoService.ts b/lib/services/repoService.ts index d02b0cc..e6f8e4e 100644 --- a/lib/services/repoService.ts +++ b/lib/services/repoService.ts @@ -7,6 +7,7 @@ import { execCommand, TtydExecError } from '@/lib/util/ttyd-exec' export type RepoInitResult = { success: boolean message: string + code?: string } /** @@ -69,7 +70,7 @@ export async function initializeRepo(projectId: string): Promise // Create GitHub repo first const repoResult = await createGithubRepo(project.name) if (!repoResult.success) { - return { success: false, message: repoResult.message } + return { success: false, message: repoResult.message, code: repoResult.code } } // Save repo URL to database @@ -110,6 +111,7 @@ export type CreateRepoResult = { message: string repoUrl?: string cloneUrl?: string + code?: string } /** @@ -133,14 +135,14 @@ export async function createGithubRepo(repoName: string): Promise // Push changes to GitHub const pushResult = await pushToGithub(projectId) if (!pushResult.success) { + // If authentication failed, we should return failure so the UI can prompt for binding + if (pushResult.code === 'GITHUB_NOT_BOUND') { + return { success: false, message: pushResult.message, code: pushResult.code } + } return { success: true, message: `Committed locally but failed to push: ${pushResult.message}` } } @@ -248,7 +254,7 @@ export async function pushToGithub(projectId: string): Promise { const githubToken = metadata?.token if (!githubToken) { - return { success: false, message: 'GitHub token not found' } + return { success: false, message: 'GitHub token not found', code: 'GITHUB_NOT_BOUND' } } // Validate GitHub URL format to prevent injection attacks From 1c09474b3aea8480fb6bda7d6d830357a6bb5d69 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:25:08 +0800 Subject: [PATCH 07/10] chore(deps): bump next.js to 16.0.10 for security fixes Addresses CVE-2025-55184 and CVE-2025-55183. Ref: https://vercel.com/kb/bulletin/security-bulletin-cve-2025-55184-and-cve-2025-55183 --- package.json | 2 +- pnpm-lock.yaml | 88 +++++++++++++++++++++++++------------------------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index cb902d2..358918a 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "jsonwebtoken": "^9.0.2", "lucide-react": "^0.545.0", "nanoid": "^5.1.6", - "next": "16.0.7", + "next": "16.0.10", "next-auth": "^5.0.0-beta.29", "next-themes": "^0.4.6", "pino": "^10.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cfd6e2..6e9a78e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,11 +99,11 @@ importers: specifier: ^5.1.6 version: 5.1.6 next: - specifier: 16.0.7 - version: 16.0.7(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + specifier: 16.0.10 + version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) next-auth: specifier: ^5.0.0-beta.29 - version: 5.0.0-beta.30(next@16.0.7(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) + version: 5.0.0-beta.30(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -514,56 +514,56 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@16.0.7': - resolution: {integrity: sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==} + '@next/env@16.0.10': + resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==} '@next/eslint-plugin-next@16.0.7': resolution: {integrity: sha512-hFrTNZcMEG+k7qxVxZJq3F32Kms130FAhG8lvw2zkKBgAcNOJIxlljNiCjGygvBshvaGBdf88q2CqWtnqezDHA==} - '@next/swc-darwin-arm64@16.0.7': - resolution: {integrity: sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==} + '@next/swc-darwin-arm64@16.0.10': + resolution: {integrity: sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.0.7': - resolution: {integrity: sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==} + '@next/swc-darwin-x64@16.0.10': + resolution: {integrity: sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.0.7': - resolution: {integrity: sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==} + '@next/swc-linux-arm64-gnu@16.0.10': + resolution: {integrity: sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@16.0.7': - resolution: {integrity: sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==} + '@next/swc-linux-arm64-musl@16.0.10': + resolution: {integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@16.0.7': - resolution: {integrity: sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==} + '@next/swc-linux-x64-gnu@16.0.10': + resolution: {integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@16.0.7': - resolution: {integrity: sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==} + '@next/swc-linux-x64-musl@16.0.10': + resolution: {integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@16.0.7': - resolution: {integrity: sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==} + '@next/swc-win32-arm64-msvc@16.0.10': + resolution: {integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.0.7': - resolution: {integrity: sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==} + '@next/swc-win32-x64-msvc@16.0.10': + resolution: {integrity: sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2577,8 +2577,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.0.7: - resolution: {integrity: sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==} + next@16.0.10: + resolution: {integrity: sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -3627,34 +3627,34 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/env@16.0.7': {} + '@next/env@16.0.10': {} '@next/eslint-plugin-next@16.0.7': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@16.0.7': + '@next/swc-darwin-arm64@16.0.10': optional: true - '@next/swc-darwin-x64@16.0.7': + '@next/swc-darwin-x64@16.0.10': optional: true - '@next/swc-linux-arm64-gnu@16.0.7': + '@next/swc-linux-arm64-gnu@16.0.10': optional: true - '@next/swc-linux-arm64-musl@16.0.7': + '@next/swc-linux-arm64-musl@16.0.10': optional: true - '@next/swc-linux-x64-gnu@16.0.7': + '@next/swc-linux-x64-gnu@16.0.10': optional: true - '@next/swc-linux-x64-musl@16.0.7': + '@next/swc-linux-x64-musl@16.0.10': optional: true - '@next/swc-win32-arm64-msvc@16.0.7': + '@next/swc-win32-arm64-msvc@16.0.10': optional: true - '@next/swc-win32-x64-msvc@16.0.7': + '@next/swc-win32-x64-msvc@16.0.10': optional: true '@nodelib/fs.scandir@2.1.5': @@ -5714,10 +5714,10 @@ snapshots: natural-compare@1.4.0: {} - next-auth@5.0.0-beta.30(next@16.0.7(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1): + next-auth@5.0.0-beta.30(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react@19.2.1): dependencies: '@auth/core': 0.41.0 - next: 16.0.7(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + next: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) react: 19.2.1 next-themes@0.4.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1): @@ -5725,9 +5725,9 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) - next@16.0.7(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): + next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): dependencies: - '@next/env': 16.0.7 + '@next/env': 16.0.10 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001759 postcss: 8.4.31 @@ -5735,14 +5735,14 @@ snapshots: react-dom: 19.2.1(react@19.2.1) styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.1) optionalDependencies: - '@next/swc-darwin-arm64': 16.0.7 - '@next/swc-darwin-x64': 16.0.7 - '@next/swc-linux-arm64-gnu': 16.0.7 - '@next/swc-linux-arm64-musl': 16.0.7 - '@next/swc-linux-x64-gnu': 16.0.7 - '@next/swc-linux-x64-musl': 16.0.7 - '@next/swc-win32-arm64-msvc': 16.0.7 - '@next/swc-win32-x64-msvc': 16.0.7 + '@next/swc-darwin-arm64': 16.0.10 + '@next/swc-darwin-x64': 16.0.10 + '@next/swc-linux-arm64-gnu': 16.0.10 + '@next/swc-linux-arm64-musl': 16.0.10 + '@next/swc-linux-x64-gnu': 16.0.10 + '@next/swc-linux-x64-musl': 16.0.10 + '@next/swc-win32-arm64-msvc': 16.0.10 + '@next/swc-win32-x64-msvc': 16.0.10 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' From 34c6d7471c6924fbb18f86c71d56d1622a16d94e Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:56:26 +0800 Subject: [PATCH 08/10] Update lib/services/repoService.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/services/repoService.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/services/repoService.ts b/lib/services/repoService.ts index e6f8e4e..e0eade7 100644 --- a/lib/services/repoService.ts +++ b/lib/services/repoService.ts @@ -221,8 +221,6 @@ export async function commitChanges(projectId: string): Promise } } - - /** * Push local commits to GitHub * @param projectId - The ID of the project From 90fdc60e39ff422918139a9acff68b8ed19e1299 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:56:49 +0800 Subject: [PATCH 09/10] Update lib/services/repoService.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/services/repoService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/services/repoService.ts b/lib/services/repoService.ts index e0eade7..27500eb 100644 --- a/lib/services/repoService.ts +++ b/lib/services/repoService.ts @@ -103,7 +103,6 @@ async function runInitCommand(baseUrl: string, accessToken: string) { 300000 ) - } export type CreateRepoResult = { From 253e1a5dfd018c76436c81485062b3abfe0adff2 Mon Sep 17 00:00:00 2001 From: Che <30403707+Che-Zhu@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:57:27 +0800 Subject: [PATCH 10/10] Update components/layout/status-bar.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- components/layout/status-bar.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/layout/status-bar.tsx b/components/layout/status-bar.tsx index 29f1767..12fe3f7 100644 --- a/components/layout/status-bar.tsx +++ b/components/layout/status-bar.tsx @@ -17,8 +17,6 @@ interface StatusBarProps { project: ProjectWithRelations; } - - export function StatusBar({ project }: StatusBarProps) { const database = project.databases?.[0]; const dbStatus = database?.status || 'CREATING';