diff --git a/app/components/settings/connections/ConnectionsTab.tsx b/app/components/settings/connections/ConnectionsTab.tsx
index f3506f92..2c7c6cbb 100644
--- a/app/components/settings/connections/ConnectionsTab.tsx
+++ b/app/components/settings/connections/ConnectionsTab.tsx
@@ -8,11 +8,34 @@ interface GitHubUserResponse {
login: string;
avatar_url: string;
html_url: string;
+ name: string;
+ bio: string;
+ public_repos: number;
+ followers: number;
+ following: number;
+}
+
+interface GitHubRepoInfo {
+ name: string;
+ full_name: string;
+ html_url: string;
+ description: string;
+ stargazers_count: number;
+ forks_count: number;
+ default_branch: string;
+ updated_at: string;
+}
+
+interface GitHubStats {
+ repos: GitHubRepoInfo[];
+ totalStars: number;
+ totalForks: number;
}
interface GitHubConnection {
user: GitHubUserResponse | null;
token: string;
+ stats?: GitHubStats;
}
export default function ConnectionsTab() {
@@ -22,18 +45,59 @@ export default function ConnectionsTab() {
});
const [isLoading, setIsLoading] = useState(true);
const [isConnecting, setIsConnecting] = useState(false);
+ const [isFetchingStats, setIsFetchingStats] = useState(false);
// Load saved connection on mount
useEffect(() => {
const savedConnection = localStorage.getItem('github_connection');
if (savedConnection) {
- setConnection(JSON.parse(savedConnection));
+ const parsed = JSON.parse(savedConnection);
+ setConnection(parsed);
+ if (parsed.user && parsed.token) {
+ fetchGitHubStats(parsed.token);
+ }
}
setIsLoading(false);
}, []);
+ const fetchGitHubStats = async (token: string) => {
+ try {
+ setIsFetchingStats(true);
+
+ // Fetch repositories
+ const reposResponse = await fetch('https://api.github.com/user/repos?sort=updated&per_page=10', {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (!reposResponse.ok) throw new Error('Failed to fetch repositories');
+
+ const repos = await reposResponse.json() as GitHubRepoInfo[];
+
+ // Calculate total stats
+ const totalStars = repos.reduce((acc, repo) => acc + repo.stargazers_count, 0);
+ const totalForks = repos.reduce((acc, repo) => acc + repo.forks_count, 0);
+
+ setConnection(prev => ({
+ ...prev,
+ stats: {
+ repos,
+ totalStars,
+ totalForks,
+ },
+ }));
+
+ } catch (error) {
+ logStore.logError('Failed to fetch GitHub stats', { error });
+ toast.error('Failed to fetch GitHub statistics');
+ } finally {
+ setIsFetchingStats(false);
+ }
+ };
+
const fetchGithubUser = async (token: string) => {
try {
setIsConnecting(true);
@@ -44,16 +108,18 @@ export default function ConnectionsTab() {
},
});
- if (!response.ok) {
- throw new Error('Invalid token or unauthorized');
- }
+ if (!response.ok) throw new Error('Invalid token or unauthorized');
- const data = (await response.json()) as GitHubUserResponse;
+ const data = await response.json() as GitHubUserResponse;
const newConnection = { user: data, token };
// Save connection
localStorage.setItem('github_connection', JSON.stringify(newConnection));
setConnection(newConnection);
+
+ // Fetch additional stats
+ await fetchGitHubStats(token);
+
toast.success('Successfully connected to GitHub');
} catch (error) {
logStore.logError('Failed to authenticate with GitHub', { error });
@@ -75,16 +141,7 @@ export default function ConnectionsTab() {
toast.success('Disconnected from GitHub');
};
- if (isLoading) {
- return (
-
- );
- }
+ if (isLoading) return ;
return (
@@ -200,9 +257,92 @@ export default function ConnectionsTab() {
)}
+
+ {connection.user && connection.stats && (
+
+
+

+
+
+ {connection.user.name || connection.user.login}
+
+ {connection.user.bio && (
+
{connection.user.bio}
+ )}
+
+
+
+ {connection.user.followers} followers
+
+
+
+ {connection.stats.totalStars} stars
+
+
+
+ {connection.stats.totalForks} forks
+
+
+
+
+
+
+ Recent Repositories
+
+
+
+ )}
);
}
+
+function LoadingSpinner() {
+ return (
+
+ );
+}
diff --git a/app/components/settings/debug/DebugTab.tsx b/app/components/settings/debug/DebugTab.tsx
index 9238fb85..9ee8827a 100644
--- a/app/components/settings/debug/DebugTab.tsx
+++ b/app/components/settings/debug/DebugTab.tsx
@@ -84,6 +84,7 @@ interface SystemInfo {
}
interface WebAppInfo {
+ // Local WebApp Info
name: string;
version: string;
description: string;
@@ -91,6 +92,33 @@ interface WebAppInfo {
nodeVersion: string;
dependencies: { [key: string]: string };
devDependencies: { [key: string]: string };
+ // Build Info
+ buildTime?: string;
+ buildNumber?: string;
+ environment?: string;
+ // Git Info
+ gitInfo?: {
+ branch: string;
+ commit: string;
+ commitTime: string;
+ author: string;
+ remoteUrl: string;
+ };
+ // GitHub Repository Info
+ repoInfo?: {
+ name: string;
+ fullName: string;
+ description: string;
+ stars: number;
+ forks: number;
+ openIssues: number;
+ defaultBranch: string;
+ lastUpdate: string;
+ owner: {
+ login: string;
+ avatarUrl: string;
+ };
+ };
}
export default function DebugTab() {
@@ -298,14 +326,54 @@ export default function DebugTab() {
try {
setLoading((prev) => ({ ...prev, webAppInfo: true }));
- const response = await fetch('/api/system/app-info');
-
- if (!response.ok) {
+ // Fetch local app info
+ const appInfoResponse = await fetch('/api/system/app-info');
+ if (!appInfoResponse.ok) {
throw new Error('Failed to fetch webapp info');
}
+ const appData = await appInfoResponse.json();
- const data = await response.json();
- setWebAppInfo(data as WebAppInfo);
+ // Fetch git info
+ const gitInfoResponse = await fetch('/api/system/git-info');
+ let gitInfo = null;
+ if (gitInfoResponse.ok) {
+ gitInfo = await gitInfoResponse.json();
+ }
+
+ // Fetch GitHub repository info
+ const repoInfoResponse = await fetch('https://api.github.com/repos/stackblitz-labs/bolt.diy');
+ let repoInfo = null;
+ if (repoInfoResponse.ok) {
+ const repoData = await repoInfoResponse.json();
+ repoInfo = {
+ name: repoData.name,
+ fullName: repoData.full_name,
+ description: repoData.description,
+ stars: repoData.stargazers_count,
+ forks: repoData.forks_count,
+ openIssues: repoData.open_issues_count,
+ defaultBranch: repoData.default_branch,
+ lastUpdate: repoData.updated_at,
+ owner: {
+ login: repoData.owner.login,
+ avatarUrl: repoData.owner.avatar_url,
+ },
+ };
+ }
+
+ // Get build info from environment variables or config
+ const buildInfo = {
+ buildTime: process.env.NEXT_PUBLIC_BUILD_TIME || new Date().toISOString(),
+ buildNumber: process.env.NEXT_PUBLIC_BUILD_NUMBER || 'development',
+ environment: process.env.NEXT_PUBLIC_ENV || 'development',
+ };
+
+ setWebAppInfo({
+ ...appData,
+ ...buildInfo,
+ gitInfo,
+ repoInfo,
+ });
} catch (error) {
console.error('Failed to fetch webapp info:', error);
toast.error('Failed to fetch webapp information');
@@ -895,6 +963,27 @@ export default function DebugTab() {
Node Version:
{webAppInfo.nodeVersion}
+ {webAppInfo.buildTime && (
+
+
+
Build Time:
+
{webAppInfo.buildTime}
+
+ )}
+ {webAppInfo.buildNumber && (
+
+
+
Build Number:
+
{webAppInfo.buildNumber}
+
+ )}
+ {webAppInfo.environment && (
+
+
+
Environment:
+
{webAppInfo.environment}
+
+ )}
@@ -912,6 +1001,71 @@ export default function DebugTab() {
))}
+ {webAppInfo.gitInfo && (
+
+
+
+
+ Branch: {webAppInfo.gitInfo.branch}
+
+
+ Commit: {webAppInfo.gitInfo.commit}
+
+
+ Commit Time: {webAppInfo.gitInfo.commitTime}
+
+
+ Author: {webAppInfo.gitInfo.author}
+
+
+ Remote URL: {webAppInfo.gitInfo.remoteUrl}
+
+
+
+ )}
+ {webAppInfo.repoInfo && (
+
+
+
+
+ Name: {webAppInfo.repoInfo.name}
+
+
+ Full Name: {webAppInfo.repoInfo.fullName}
+
+
+ Description: {webAppInfo.repoInfo.description}
+
+
+ Stars: {webAppInfo.repoInfo.stars}
+
+
+ Forks: {webAppInfo.repoInfo.forks}
+
+
+ Open Issues: {webAppInfo.repoInfo.openIssues}
+
+
+ Default Branch: {webAppInfo.repoInfo.defaultBranch}
+
+
+ Last Update: {webAppInfo.repoInfo.lastUpdate}
+
+
+ Owner: {webAppInfo.repoInfo.owner.login}
+
+
+ Avatar URL: {webAppInfo.repoInfo.owner.avatarUrl}
+
+
+
+ )}
) : (
diff --git a/app/routes/api.system.git-info.ts b/app/routes/api.system.git-info.ts
new file mode 100644
index 00000000..e30c2626
--- /dev/null
+++ b/app/routes/api.system.git-info.ts
@@ -0,0 +1,26 @@
+import { json } from '@remix-run/node';
+import type { LoaderFunctionArgs } from '@remix-run/node';
+import { execSync } from 'child_process';
+
+export async function loader({ request }: LoaderFunctionArgs) {
+ try {
+ const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
+ const commit = execSync('git rev-parse --short HEAD').toString().trim();
+ const lastCommitMessage = execSync('git log -1 --pretty=%B').toString().trim();
+
+ return json({
+ branch,
+ commit,
+ lastCommitMessage,
+ timestamp: new Date().toISOString(),
+ });
+ } catch (error) {
+ return json(
+ {
+ error: 'Failed to fetch git information',
+ details: error instanceof Error ? error.message : 'Unknown error',
+ },
+ { status: 500 },
+ );
+ }
+}
diff --git a/package.json b/package.json
index 8a2d24e2..6ea47a5d 100644
--- a/package.json
+++ b/package.json
@@ -67,6 +67,7 @@
"@radix-ui/react-tooltip": "^1.1.4",
"@remix-run/cloudflare": "^2.15.0",
"@remix-run/cloudflare-pages": "^2.15.0",
+ "@remix-run/node": "^2.15.2",
"@remix-run/react": "^2.15.0",
"@uiw/codemirror-theme-vscode": "^4.23.6",
"@unocss/reset": "^0.61.9",
diff --git a/pages/api/system/git-info.ts b/pages/api/system/git-info.ts
new file mode 100644
index 00000000..02619108
--- /dev/null
+++ b/pages/api/system/git-info.ts
@@ -0,0 +1,30 @@
+import { NextApiRequest, NextApiResponse } from 'next';
+import { execSync } from 'child_process';
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ if (req.method !== 'GET') {
+ return res.status(405).json({ message: 'Method not allowed' });
+ }
+
+ try {
+ // Get git information using git commands
+ const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
+ const commit = execSync('git rev-parse HEAD').toString().trim();
+ const commitTime = execSync('git log -1 --format=%cd').toString().trim();
+ const author = execSync('git log -1 --format=%an').toString().trim();
+ const remoteUrl = execSync('git config --get remote.origin.url').toString().trim();
+
+ const gitInfo = {
+ branch,
+ commit,
+ commitTime,
+ author,
+ remoteUrl,
+ };
+
+ res.status(200).json(gitInfo);
+ } catch (error) {
+ console.error('Failed to get git information:', error);
+ res.status(500).json({ message: 'Failed to get git information' });
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c3b6c4bf..b1714f4d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -122,6 +122,9 @@ importers:
'@remix-run/cloudflare-pages':
specifier: ^2.15.0
version: 2.15.0(@cloudflare/workers-types@4.20241127.0)(typescript@5.7.2)
+ '@remix-run/node':
+ specifier: ^2.15.2
+ version: 2.15.2(typescript@5.7.2)
'@remix-run/react':
specifier: ^2.15.0
version: 2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)
@@ -241,10 +244,10 @@ importers:
version: 4.0.0
remix-island:
specifier: ^0.2.0
- version: 0.2.0(@remix-run/react@2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@remix-run/server-runtime@2.15.0(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ version: 0.2.0(@remix-run/react@2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@remix-run/server-runtime@2.15.2(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
remix-utils:
specifier: ^7.7.0
- version: 7.7.0(@remix-run/cloudflare@2.15.0(@cloudflare/workers-types@4.20241127.0)(typescript@5.7.2))(@remix-run/node@2.15.0(typescript@5.7.2))(@remix-run/react@2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@remix-run/router@1.21.0)(react@18.3.1)(zod@3.23.8)
+ version: 7.7.0(@remix-run/cloudflare@2.15.0(@cloudflare/workers-types@4.20241127.0)(typescript@5.7.2))(@remix-run/node@2.15.2(typescript@5.7.2))(@remix-run/react@2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@remix-run/router@1.21.0)(react@18.3.1)(zod@3.23.8)
shiki:
specifier: ^1.24.0
version: 1.24.0
@@ -2294,6 +2297,15 @@ packages:
typescript:
optional: true
+ '@remix-run/node@2.15.2':
+ resolution: {integrity: sha512-NS/h5uxje7DYCNgcKqKAiUhf0r2HVnoYUBWLyIIMmCUP1ddWurBP6xTPcWzGhEvV/EvguniYi1wJZ5+X8sonWw==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ typescript: ^5.1.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
'@remix-run/react@2.15.0':
resolution: {integrity: sha512-puqDbi9N/WfaUhzDnw2pACXtCB7ukrtFJ9ILwpEuhlaTBpjefifJ89igokW+tt1ePphIFMivAm/YspcbZdCQsA==}
engines: {node: '>=18.0.0'}
@@ -2318,6 +2330,15 @@ packages:
typescript:
optional: true
+ '@remix-run/server-runtime@2.15.2':
+ resolution: {integrity: sha512-OqiPcvEnnU88B8b1LIWHHkQ3Tz2GDAmQ1RihFNQsbrFKpDsQLkw0lJlnfgKA/uHd0CEEacpfV7C9qqJT3V6Z2g==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ typescript: ^5.1.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
'@remix-run/web-blob@3.1.0':
resolution: {integrity: sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==}
@@ -8640,6 +8661,18 @@ snapshots:
optionalDependencies:
typescript: 5.7.2
+ '@remix-run/node@2.15.2(typescript@5.7.2)':
+ dependencies:
+ '@remix-run/server-runtime': 2.15.2(typescript@5.7.2)
+ '@remix-run/web-fetch': 4.4.2
+ '@web3-storage/multipart-parser': 1.0.0
+ cookie-signature: 1.2.2
+ source-map-support: 0.5.21
+ stream-slice: 0.1.2
+ undici: 6.21.0
+ optionalDependencies:
+ typescript: 5.7.2
+
'@remix-run/react@2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)':
dependencies:
'@remix-run/router': 1.21.0
@@ -8666,6 +8699,18 @@ snapshots:
optionalDependencies:
typescript: 5.7.2
+ '@remix-run/server-runtime@2.15.2(typescript@5.7.2)':
+ dependencies:
+ '@remix-run/router': 1.21.0
+ '@types/cookie': 0.6.0
+ '@web3-storage/multipart-parser': 1.0.0
+ cookie: 0.6.0
+ set-cookie-parser: 2.7.1
+ source-map: 0.7.4
+ turbo-stream: 2.4.0
+ optionalDependencies:
+ typescript: 5.7.2
+
'@remix-run/web-blob@3.1.0':
dependencies:
'@remix-run/web-stream': 1.1.0
@@ -12680,19 +12725,19 @@ snapshots:
mdast-util-to-markdown: 2.1.2
unified: 11.0.5
- remix-island@0.2.0(@remix-run/react@2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@remix-run/server-runtime@2.15.0(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ remix-island@0.2.0(@remix-run/react@2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@remix-run/server-runtime@2.15.2(typescript@5.7.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@remix-run/react': 2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)
- '@remix-run/server-runtime': 2.15.0(typescript@5.7.2)
+ '@remix-run/server-runtime': 2.15.2(typescript@5.7.2)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
- remix-utils@7.7.0(@remix-run/cloudflare@2.15.0(@cloudflare/workers-types@4.20241127.0)(typescript@5.7.2))(@remix-run/node@2.15.0(typescript@5.7.2))(@remix-run/react@2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@remix-run/router@1.21.0)(react@18.3.1)(zod@3.23.8):
+ remix-utils@7.7.0(@remix-run/cloudflare@2.15.0(@cloudflare/workers-types@4.20241127.0)(typescript@5.7.2))(@remix-run/node@2.15.2(typescript@5.7.2))(@remix-run/react@2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2))(@remix-run/router@1.21.0)(react@18.3.1)(zod@3.23.8):
dependencies:
type-fest: 4.30.0
optionalDependencies:
'@remix-run/cloudflare': 2.15.0(@cloudflare/workers-types@4.20241127.0)(typescript@5.7.2)
- '@remix-run/node': 2.15.0(typescript@5.7.2)
+ '@remix-run/node': 2.15.2(typescript@5.7.2)
'@remix-run/react': 2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)
'@remix-run/router': 1.21.0
react: 18.3.1