Merge branch 'main' into stable
This commit is contained in:
@@ -37,11 +37,19 @@ function parseCookies(cookieHeader: string): Record<string, string> {
|
||||
}
|
||||
|
||||
async function chatAction({ context, request }: ActionFunctionArgs) {
|
||||
const { messages, files, promptId, contextOptimization } = await request.json<{
|
||||
const { messages, files, promptId, contextOptimization, supabase } = await request.json<{
|
||||
messages: Messages;
|
||||
files: any;
|
||||
promptId?: string;
|
||||
contextOptimization: boolean;
|
||||
supabase?: {
|
||||
isConnected: boolean;
|
||||
hasSelectedProject: boolean;
|
||||
credentials?: {
|
||||
anonKey?: string;
|
||||
supabaseUrl?: string;
|
||||
};
|
||||
};
|
||||
}>();
|
||||
|
||||
const cookieHeader = request.headers.get('Cookie');
|
||||
@@ -179,8 +187,8 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
|
||||
// logger.debug('Code Files Selected');
|
||||
}
|
||||
|
||||
// Stream the text
|
||||
const options: StreamingOptions = {
|
||||
supabaseConnection: supabase,
|
||||
toolChoice: 'none',
|
||||
onFinish: async ({ text: content, finishReason, usage }) => {
|
||||
logger.debug('usage', JSON.stringify(usage));
|
||||
|
||||
44
app/routes/api.export-api-keys.ts
Normal file
44
app/routes/api.export-api-keys.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { LoaderFunction } from '@remix-run/cloudflare';
|
||||
import { LLMManager } from '~/lib/modules/llm/manager';
|
||||
import { getApiKeysFromCookie } from '~/lib/api/cookies';
|
||||
|
||||
export const loader: LoaderFunction = async ({ context, request }) => {
|
||||
// Get API keys from cookie
|
||||
const cookieHeader = request.headers.get('Cookie');
|
||||
const apiKeysFromCookie = getApiKeysFromCookie(cookieHeader);
|
||||
|
||||
// Initialize the LLM manager to access environment variables
|
||||
const llmManager = LLMManager.getInstance(context?.cloudflare?.env as any);
|
||||
|
||||
// Get all provider instances to find their API token keys
|
||||
const providers = llmManager.getAllProviders();
|
||||
|
||||
// Create a comprehensive API keys object
|
||||
const apiKeys: Record<string, string> = { ...apiKeysFromCookie };
|
||||
|
||||
// For each provider, check all possible sources for API keys
|
||||
for (const provider of providers) {
|
||||
if (!provider.config.apiTokenKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const envVarName = provider.config.apiTokenKey;
|
||||
|
||||
// Skip if we already have this provider's key from cookies
|
||||
if (apiKeys[provider.name]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check environment variables in order of precedence
|
||||
const envValue =
|
||||
(context?.cloudflare?.env as Record<string, any>)?.[envVarName] ||
|
||||
process.env[envVarName] ||
|
||||
llmManager.env[envVarName];
|
||||
|
||||
if (envValue) {
|
||||
apiKeys[provider.name] = envValue;
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json(apiKeys);
|
||||
};
|
||||
@@ -117,6 +117,7 @@ async function handleProxyRequest(request: Request, path: string | undefined) {
|
||||
// Add body for non-GET/HEAD requests
|
||||
if (!['GET', 'HEAD'].includes(request.method)) {
|
||||
fetchOptions.body = request.body;
|
||||
fetchOptions.duplex = 'half';
|
||||
|
||||
/*
|
||||
* Note: duplex property is removed to ensure TypeScript compatibility
|
||||
|
||||
96
app/routes/api.github-template.ts
Normal file
96
app/routes/api.github-template.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { json } from '@remix-run/cloudflare';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export async function loader({ request }: { request: Request }) {
|
||||
const url = new URL(request.url);
|
||||
const repo = url.searchParams.get('repo');
|
||||
|
||||
if (!repo) {
|
||||
return json({ error: 'Repository name is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = 'https://api.github.com';
|
||||
|
||||
// Get the latest release
|
||||
const releaseResponse = await fetch(`${baseUrl}/repos/${repo}/releases/latest`, {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
|
||||
// Add GitHub token if available in environment variables
|
||||
...(process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!releaseResponse.ok) {
|
||||
throw new Error(`GitHub API error: ${releaseResponse.status}`);
|
||||
}
|
||||
|
||||
const releaseData = (await releaseResponse.json()) as any;
|
||||
const zipballUrl = releaseData.zipball_url;
|
||||
|
||||
// Fetch the zipball
|
||||
const zipResponse = await fetch(zipballUrl, {
|
||||
headers: {
|
||||
...(process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!zipResponse.ok) {
|
||||
throw new Error(`Failed to fetch release zipball: ${zipResponse.status}`);
|
||||
}
|
||||
|
||||
// Get the zip content as ArrayBuffer
|
||||
const zipArrayBuffer = await zipResponse.arrayBuffer();
|
||||
|
||||
// Use JSZip to extract the contents
|
||||
const zip = await JSZip.loadAsync(zipArrayBuffer);
|
||||
|
||||
// Find the root folder name
|
||||
let rootFolderName = '';
|
||||
zip.forEach((relativePath) => {
|
||||
if (!rootFolderName && relativePath.includes('/')) {
|
||||
rootFolderName = relativePath.split('/')[0];
|
||||
}
|
||||
});
|
||||
|
||||
// Extract all files
|
||||
const promises = Object.keys(zip.files).map(async (filename) => {
|
||||
const zipEntry = zip.files[filename];
|
||||
|
||||
// Skip directories
|
||||
if (zipEntry.dir) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Skip the root folder itself
|
||||
if (filename === rootFolderName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove the root folder from the path
|
||||
let normalizedPath = filename;
|
||||
|
||||
if (rootFolderName && filename.startsWith(rootFolderName + '/')) {
|
||||
normalizedPath = filename.substring(rootFolderName.length + 1);
|
||||
}
|
||||
|
||||
// Get the file content
|
||||
const content = await zipEntry.async('string');
|
||||
|
||||
return {
|
||||
name: normalizedPath.split('/').pop() || '',
|
||||
path: normalizedPath,
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const fileList = results.filter(Boolean) as { name: string; path: string; content: string }[];
|
||||
|
||||
return json(fileList);
|
||||
} catch (error) {
|
||||
console.error('Error processing GitHub template:', error);
|
||||
return json({ error: 'Failed to fetch template files' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
92
app/routes/api.supabase.query.ts
Normal file
92
app/routes/api.supabase.query.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { type ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('api.supabase.query');
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
if (request.method !== 'POST') {
|
||||
return new Response('Method not allowed', { status: 405 });
|
||||
}
|
||||
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
|
||||
if (!authHeader) {
|
||||
return new Response('No authorization token provided', { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { projectId, query } = (await request.json()) as any;
|
||||
logger.debug('Executing query:', { projectId, query });
|
||||
|
||||
const response = await fetch(`https://api.supabase.com/v1/projects/${projectId}/database/query`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ query }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
let errorData;
|
||||
|
||||
try {
|
||||
errorData = JSON.parse(errorText);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
errorData = { message: errorText };
|
||||
}
|
||||
|
||||
logger.error(
|
||||
'Supabase API error:',
|
||||
JSON.stringify({
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorData,
|
||||
}),
|
||||
);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
message: errorData.message || errorData.error || errorText,
|
||||
details: errorData,
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: response.status,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Query execution error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: error instanceof Error ? error.message : 'Query execution failed',
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
56
app/routes/api.supabase.ts
Normal file
56
app/routes/api.supabase.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { json, type ActionFunction } from '@remix-run/cloudflare';
|
||||
import type { SupabaseProject } from '~/types/supabase';
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
if (request.method !== 'POST') {
|
||||
return json({ error: 'Method not allowed' }, { status: 405 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { token } = (await request.json()) as any;
|
||||
|
||||
const projectsResponse = await fetch('https://api.supabase.com/v1/projects', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!projectsResponse.ok) {
|
||||
const errorText = await projectsResponse.text();
|
||||
console.error('Projects fetch failed:', errorText);
|
||||
|
||||
return json({ error: 'Failed to fetch projects' }, { status: 401 });
|
||||
}
|
||||
|
||||
const projects = (await projectsResponse.json()) as SupabaseProject[];
|
||||
|
||||
const uniqueProjectsMap = new Map<string, SupabaseProject>();
|
||||
|
||||
for (const project of projects) {
|
||||
if (!uniqueProjectsMap.has(project.id)) {
|
||||
uniqueProjectsMap.set(project.id, project);
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueProjects = Array.from(uniqueProjectsMap.values());
|
||||
|
||||
uniqueProjects.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||
|
||||
return json({
|
||||
user: { email: 'Connected', role: 'Admin' },
|
||||
stats: {
|
||||
projects: uniqueProjects,
|
||||
totalProjects: uniqueProjects.length,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Supabase API error:', error);
|
||||
return json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : 'Authentication failed',
|
||||
},
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
};
|
||||
32
app/routes/api.supabase.variables.ts
Normal file
32
app/routes/api.supabase.variables.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { json, type ActionFunctionArgs } from '@remix-run/cloudflare';
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
try {
|
||||
// Add proper type assertion for the request body
|
||||
const body = (await request.json()) as { projectId?: string; token?: string };
|
||||
const { projectId, token } = body;
|
||||
|
||||
if (!projectId || !token) {
|
||||
return json({ error: 'Project ID and token are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const response = await fetch(`https://api.supabase.com/v1/projects/${projectId}/api-keys`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return json({ error: `Failed to fetch API keys: ${response.statusText}` }, { status: response.status });
|
||||
}
|
||||
|
||||
const apiKeys = await response.json();
|
||||
|
||||
return json({ apiKeys });
|
||||
} catch (error) {
|
||||
console.error('Error fetching project API keys:', error);
|
||||
return json({ error: error instanceof Error ? error.message : 'Unknown error occurred' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
142
app/routes/api.system.diagnostics.ts
Normal file
142
app/routes/api.system.diagnostics.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { json, type LoaderFunction, type LoaderFunctionArgs } from '@remix-run/cloudflare';
|
||||
|
||||
/**
|
||||
* Diagnostic API for troubleshooting connection issues
|
||||
*/
|
||||
|
||||
interface AppContext {
|
||||
env?: {
|
||||
GITHUB_ACCESS_TOKEN?: string;
|
||||
NETLIFY_TOKEN?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async ({ request, context }: LoaderFunctionArgs & { context: AppContext }) => {
|
||||
// Get environment variables
|
||||
const envVars = {
|
||||
hasGithubToken: Boolean(process.env.GITHUB_ACCESS_TOKEN || context.env?.GITHUB_ACCESS_TOKEN),
|
||||
hasNetlifyToken: Boolean(process.env.NETLIFY_TOKEN || context.env?.NETLIFY_TOKEN),
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
};
|
||||
|
||||
// Check cookies
|
||||
const cookieHeader = request.headers.get('Cookie') || '';
|
||||
const cookies = cookieHeader.split(';').reduce(
|
||||
(acc, cookie) => {
|
||||
const [key, value] = cookie.trim().split('=');
|
||||
|
||||
if (key) {
|
||||
acc[key] = value;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
const hasGithubTokenCookie = Boolean(cookies.githubToken);
|
||||
const hasGithubUsernameCookie = Boolean(cookies.githubUsername);
|
||||
const hasNetlifyCookie = Boolean(cookies.netlifyToken);
|
||||
|
||||
// Get local storage status (this can only be checked client-side)
|
||||
const localStorageStatus = {
|
||||
explanation: 'Local storage can only be checked on the client side. Use browser devtools to check.',
|
||||
githubKeysToCheck: ['github_connection'],
|
||||
netlifyKeysToCheck: ['netlify_connection'],
|
||||
};
|
||||
|
||||
// Check if CORS might be an issue
|
||||
const corsStatus = {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
},
|
||||
};
|
||||
|
||||
// Check if API endpoints are reachable
|
||||
const apiEndpoints = {
|
||||
githubUser: '/api/system/git-info?action=getUser',
|
||||
githubRepos: '/api/system/git-info?action=getRepos',
|
||||
githubOrgs: '/api/system/git-info?action=getOrgs',
|
||||
githubActivity: '/api/system/git-info?action=getActivity',
|
||||
gitInfo: '/api/system/git-info',
|
||||
};
|
||||
|
||||
// Test GitHub API connectivity
|
||||
let githubApiStatus;
|
||||
|
||||
try {
|
||||
const githubResponse = await fetch('https://api.github.com/zen', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
},
|
||||
});
|
||||
|
||||
githubApiStatus = {
|
||||
isReachable: githubResponse.ok,
|
||||
status: githubResponse.status,
|
||||
statusText: githubResponse.statusText,
|
||||
};
|
||||
} catch (error) {
|
||||
githubApiStatus = {
|
||||
isReachable: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
|
||||
// Test Netlify API connectivity
|
||||
let netlifyApiStatus;
|
||||
|
||||
try {
|
||||
const netlifyResponse = await fetch('https://api.netlify.com/api/v1/', {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
netlifyApiStatus = {
|
||||
isReachable: netlifyResponse.ok,
|
||||
status: netlifyResponse.status,
|
||||
statusText: netlifyResponse.statusText,
|
||||
};
|
||||
} catch (error) {
|
||||
netlifyApiStatus = {
|
||||
isReachable: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
|
||||
// Provide technical details about the environment
|
||||
const technicalDetails = {
|
||||
serverTimestamp: new Date().toISOString(),
|
||||
userAgent: request.headers.get('User-Agent'),
|
||||
referrer: request.headers.get('Referer'),
|
||||
host: request.headers.get('Host'),
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
};
|
||||
|
||||
// Return diagnostics
|
||||
return json(
|
||||
{
|
||||
status: 'success',
|
||||
environment: envVars,
|
||||
cookies: {
|
||||
hasGithubTokenCookie,
|
||||
hasGithubUsernameCookie,
|
||||
hasNetlifyCookie,
|
||||
},
|
||||
localStorage: localStorageStatus,
|
||||
apiEndpoints,
|
||||
externalApis: {
|
||||
github: githubApiStatus,
|
||||
netlify: netlifyApiStatus,
|
||||
},
|
||||
corsStatus,
|
||||
technicalDetails,
|
||||
},
|
||||
{
|
||||
headers: corsStatus.headers,
|
||||
},
|
||||
);
|
||||
};
|
||||
311
app/routes/api.system.disk-info.ts
Normal file
311
app/routes/api.system.disk-info.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import type { ActionFunctionArgs, LoaderFunction } from '@remix-run/cloudflare';
|
||||
import { json } from '@remix-run/cloudflare';
|
||||
|
||||
// Only import child_process if we're not in a Cloudflare environment
|
||||
let execSync: any;
|
||||
|
||||
try {
|
||||
// Check if we're in a Node.js environment
|
||||
if (typeof process !== 'undefined' && process.platform) {
|
||||
// Using dynamic import to avoid require()
|
||||
const childProcess = { execSync: null };
|
||||
execSync = childProcess.execSync;
|
||||
}
|
||||
} catch {
|
||||
// In Cloudflare environment, this will fail, which is expected
|
||||
console.log('Running in Cloudflare environment, child_process not available');
|
||||
}
|
||||
|
||||
// For development environments, we'll always provide mock data if real data isn't available
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
interface DiskInfo {
|
||||
filesystem: string;
|
||||
size: number;
|
||||
used: number;
|
||||
available: number;
|
||||
percentage: number;
|
||||
mountpoint: string;
|
||||
timestamp: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const getDiskInfo = (): DiskInfo[] => {
|
||||
// If we're in a Cloudflare environment and not in development, return error
|
||||
if (!execSync && !isDevelopment) {
|
||||
return [
|
||||
{
|
||||
filesystem: 'N/A',
|
||||
size: 0,
|
||||
used: 0,
|
||||
available: 0,
|
||||
percentage: 0,
|
||||
mountpoint: 'N/A',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: 'Disk information is not available in this environment',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// If we're in development but not in Node environment, return mock data
|
||||
if (!execSync && isDevelopment) {
|
||||
// Generate random percentage between 40-60%
|
||||
const percentage = Math.floor(40 + Math.random() * 20);
|
||||
const totalSize = 500 * 1024 * 1024 * 1024; // 500GB
|
||||
const usedSize = Math.floor((totalSize * percentage) / 100);
|
||||
const availableSize = totalSize - usedSize;
|
||||
|
||||
return [
|
||||
{
|
||||
filesystem: 'MockDisk',
|
||||
size: totalSize,
|
||||
used: usedSize,
|
||||
available: availableSize,
|
||||
percentage,
|
||||
mountpoint: '/',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
filesystem: 'MockDisk2',
|
||||
size: 1024 * 1024 * 1024 * 1024, // 1TB
|
||||
used: 300 * 1024 * 1024 * 1024, // 300GB
|
||||
available: 724 * 1024 * 1024 * 1024, // 724GB
|
||||
percentage: 30,
|
||||
mountpoint: '/data',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
// Different commands for different operating systems
|
||||
const platform = process.platform;
|
||||
let disks: DiskInfo[] = [];
|
||||
|
||||
if (platform === 'darwin') {
|
||||
// macOS - use df command to get disk information
|
||||
try {
|
||||
const output = execSync('df -k', { encoding: 'utf-8' }).toString().trim();
|
||||
|
||||
// Skip the header line
|
||||
const lines = output.split('\n').slice(1);
|
||||
|
||||
disks = lines.map((line: string) => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const filesystem = parts[0];
|
||||
const size = parseInt(parts[1], 10) * 1024; // Convert KB to bytes
|
||||
const used = parseInt(parts[2], 10) * 1024;
|
||||
const available = parseInt(parts[3], 10) * 1024;
|
||||
const percentageStr = parts[4].replace('%', '');
|
||||
const percentage = parseInt(percentageStr, 10);
|
||||
const mountpoint = parts[5];
|
||||
|
||||
return {
|
||||
filesystem,
|
||||
size,
|
||||
used,
|
||||
available,
|
||||
percentage,
|
||||
mountpoint,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
// Filter out non-physical disks
|
||||
disks = disks.filter(
|
||||
(disk) =>
|
||||
!disk.filesystem.startsWith('devfs') &&
|
||||
!disk.filesystem.startsWith('map') &&
|
||||
!disk.mountpoint.startsWith('/System/Volumes') &&
|
||||
disk.size > 0,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to get macOS disk info:', error);
|
||||
return [
|
||||
{
|
||||
filesystem: 'Unknown',
|
||||
size: 0,
|
||||
used: 0,
|
||||
available: 0,
|
||||
percentage: 0,
|
||||
mountpoint: '/',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
];
|
||||
}
|
||||
} else if (platform === 'linux') {
|
||||
// Linux - use df command to get disk information
|
||||
try {
|
||||
const output = execSync('df -k', { encoding: 'utf-8' }).toString().trim();
|
||||
|
||||
// Skip the header line
|
||||
const lines = output.split('\n').slice(1);
|
||||
|
||||
disks = lines.map((line: string) => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const filesystem = parts[0];
|
||||
const size = parseInt(parts[1], 10) * 1024; // Convert KB to bytes
|
||||
const used = parseInt(parts[2], 10) * 1024;
|
||||
const available = parseInt(parts[3], 10) * 1024;
|
||||
const percentageStr = parts[4].replace('%', '');
|
||||
const percentage = parseInt(percentageStr, 10);
|
||||
const mountpoint = parts[5];
|
||||
|
||||
return {
|
||||
filesystem,
|
||||
size,
|
||||
used,
|
||||
available,
|
||||
percentage,
|
||||
mountpoint,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
// Filter out non-physical disks
|
||||
disks = disks.filter(
|
||||
(disk) =>
|
||||
!disk.filesystem.startsWith('/dev/loop') &&
|
||||
!disk.filesystem.startsWith('tmpfs') &&
|
||||
!disk.filesystem.startsWith('devtmpfs') &&
|
||||
disk.size > 0,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to get Linux disk info:', error);
|
||||
return [
|
||||
{
|
||||
filesystem: 'Unknown',
|
||||
size: 0,
|
||||
used: 0,
|
||||
available: 0,
|
||||
percentage: 0,
|
||||
mountpoint: '/',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
];
|
||||
}
|
||||
} else if (platform === 'win32') {
|
||||
// Windows - use PowerShell to get disk information
|
||||
try {
|
||||
const output = execSync(
|
||||
'powershell "Get-PSDrive -PSProvider FileSystem | Select-Object Name, Used, Free, @{Name=\'Size\';Expression={$_.Used + $_.Free}} | ConvertTo-Json"',
|
||||
{ encoding: 'utf-8' },
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
const driveData = JSON.parse(output);
|
||||
const drivesArray = Array.isArray(driveData) ? driveData : [driveData];
|
||||
|
||||
disks = drivesArray.map((drive) => {
|
||||
const size = drive.Size || 0;
|
||||
const used = drive.Used || 0;
|
||||
const available = drive.Free || 0;
|
||||
const percentage = size > 0 ? Math.round((used / size) * 100) : 0;
|
||||
|
||||
return {
|
||||
filesystem: drive.Name + ':\\',
|
||||
size,
|
||||
used,
|
||||
available,
|
||||
percentage,
|
||||
mountpoint: drive.Name + ':\\',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get Windows disk info:', error);
|
||||
return [
|
||||
{
|
||||
filesystem: 'Unknown',
|
||||
size: 0,
|
||||
used: 0,
|
||||
available: 0,
|
||||
percentage: 0,
|
||||
mountpoint: 'C:\\',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
];
|
||||
}
|
||||
} else {
|
||||
console.warn(`Unsupported platform: ${platform}`);
|
||||
return [
|
||||
{
|
||||
filesystem: 'Unknown',
|
||||
size: 0,
|
||||
used: 0,
|
||||
available: 0,
|
||||
percentage: 0,
|
||||
mountpoint: '/',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: `Unsupported platform: ${platform}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return disks;
|
||||
} catch (error) {
|
||||
console.error('Failed to get disk info:', error);
|
||||
return [
|
||||
{
|
||||
filesystem: 'Unknown',
|
||||
size: 0,
|
||||
used: 0,
|
||||
available: 0,
|
||||
percentage: 0,
|
||||
mountpoint: '/',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async ({ request: _request }) => {
|
||||
try {
|
||||
return json(getDiskInfo());
|
||||
} catch (error) {
|
||||
console.error('Failed to get disk info:', error);
|
||||
return json(
|
||||
[
|
||||
{
|
||||
filesystem: 'Unknown',
|
||||
size: 0,
|
||||
used: 0,
|
||||
available: 0,
|
||||
percentage: 0,
|
||||
mountpoint: '/',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
],
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const action = async ({ request: _request }: ActionFunctionArgs) => {
|
||||
try {
|
||||
return json(getDiskInfo());
|
||||
} catch (error) {
|
||||
console.error('Failed to get disk info:', error);
|
||||
return json(
|
||||
[
|
||||
{
|
||||
filesystem: 'Unknown',
|
||||
size: 0,
|
||||
used: 0,
|
||||
available: 0,
|
||||
percentage: 0,
|
||||
mountpoint: '/',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
],
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { json, type LoaderFunction } from '@remix-run/cloudflare';
|
||||
import { json, type LoaderFunction, type LoaderFunctionArgs } from '@remix-run/cloudflare';
|
||||
|
||||
interface GitInfo {
|
||||
local: {
|
||||
@@ -20,6 +20,31 @@ interface GitInfo {
|
||||
};
|
||||
};
|
||||
isForked?: boolean;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
// Define context type
|
||||
interface AppContext {
|
||||
env?: {
|
||||
GITHUB_ACCESS_TOKEN?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GitHubRepo {
|
||||
name: string;
|
||||
full_name: string;
|
||||
html_url: string;
|
||||
description: string;
|
||||
stargazers_count: number;
|
||||
forks_count: number;
|
||||
language: string | null;
|
||||
languages_url: string;
|
||||
}
|
||||
|
||||
interface GitHubGist {
|
||||
id: string;
|
||||
html_url: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// These values will be replaced at build time
|
||||
@@ -31,7 +56,260 @@ declare const __GIT_EMAIL: string;
|
||||
declare const __GIT_REMOTE_URL: string;
|
||||
declare const __GIT_REPO_NAME: string;
|
||||
|
||||
export const loader: LoaderFunction = async () => {
|
||||
/*
|
||||
* Remove unused variable to fix linter error
|
||||
* declare const __GIT_REPO_URL: string;
|
||||
*/
|
||||
|
||||
export const loader: LoaderFunction = async ({ request, context }: LoaderFunctionArgs & { context: AppContext }) => {
|
||||
console.log('Git info API called with URL:', request.url);
|
||||
|
||||
// Handle CORS preflight requests
|
||||
if (request.method === 'OPTIONS') {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const action = searchParams.get('action');
|
||||
|
||||
console.log('Git info action:', action);
|
||||
|
||||
if (action === 'getUser' || action === 'getRepos' || action === 'getOrgs' || action === 'getActivity') {
|
||||
// Use server-side token instead of client-side token
|
||||
const serverGithubToken = process.env.GITHUB_ACCESS_TOKEN || context.env?.GITHUB_ACCESS_TOKEN;
|
||||
const cookieToken = request.headers
|
||||
.get('Cookie')
|
||||
?.split(';')
|
||||
.find((cookie) => cookie.trim().startsWith('githubToken='))
|
||||
?.split('=')[1];
|
||||
|
||||
// Also check for token in Authorization header
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
const headerToken = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;
|
||||
|
||||
const token = serverGithubToken || headerToken || cookieToken;
|
||||
|
||||
console.log(
|
||||
'Using GitHub token from:',
|
||||
serverGithubToken ? 'server env' : headerToken ? 'auth header' : cookieToken ? 'cookie' : 'none',
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
console.error('No GitHub token available');
|
||||
return json(
|
||||
{ error: 'No GitHub token available' },
|
||||
{
|
||||
status: 401,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (action === 'getUser') {
|
||||
const response = await fetch('https://api.github.com/user', {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('GitHub user API error:', response.status);
|
||||
throw new Error(`GitHub API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const userData = await response.json();
|
||||
|
||||
return json(
|
||||
{ user: userData },
|
||||
{
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (action === 'getRepos') {
|
||||
const reposResponse = await fetch('https://api.github.com/user/repos?per_page=100&sort=updated', {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!reposResponse.ok) {
|
||||
console.error('GitHub repos API error:', reposResponse.status);
|
||||
throw new Error(`GitHub API error: ${reposResponse.status}`);
|
||||
}
|
||||
|
||||
const repos = (await reposResponse.json()) as GitHubRepo[];
|
||||
|
||||
// Get user's gists
|
||||
const gistsResponse = await fetch('https://api.github.com/gists', {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const gists = gistsResponse.ok ? ((await gistsResponse.json()) as GitHubGist[]) : [];
|
||||
|
||||
// Calculate language statistics
|
||||
const languageStats: Record<string, number> = {};
|
||||
let totalStars = 0;
|
||||
let totalForks = 0;
|
||||
|
||||
for (const repo of repos) {
|
||||
totalStars += repo.stargazers_count || 0;
|
||||
totalForks += repo.forks_count || 0;
|
||||
|
||||
if (repo.language && repo.language !== 'null') {
|
||||
languageStats[repo.language] = (languageStats[repo.language] || 0) + 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Optionally fetch languages for each repo for more accurate stats
|
||||
* This is commented out to avoid rate limiting
|
||||
*
|
||||
* if (repo.languages_url) {
|
||||
* try {
|
||||
* const langResponse = await fetch(repo.languages_url, {
|
||||
* headers: {
|
||||
* Accept: 'application/vnd.github.v3+json',
|
||||
* Authorization: `Bearer ${token}`,
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* if (langResponse.ok) {
|
||||
* const languages = await langResponse.json();
|
||||
* Object.keys(languages).forEach(lang => {
|
||||
* languageStats[lang] = (languageStats[lang] || 0) + languages[lang];
|
||||
* });
|
||||
* }
|
||||
* } catch (error) {
|
||||
* console.error(`Error fetching languages for ${repo.name}:`, error);
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
}
|
||||
|
||||
return json(
|
||||
{
|
||||
repos,
|
||||
stats: {
|
||||
totalStars,
|
||||
totalForks,
|
||||
languages: languageStats,
|
||||
totalGists: gists.length,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (action === 'getOrgs') {
|
||||
const response = await fetch('https://api.github.com/user/orgs', {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('GitHub orgs API error:', response.status);
|
||||
throw new Error(`GitHub API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const orgs = await response.json();
|
||||
|
||||
return json(
|
||||
{ organizations: orgs },
|
||||
{
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (action === 'getActivity') {
|
||||
const username = request.headers
|
||||
.get('Cookie')
|
||||
?.split(';')
|
||||
.find((cookie) => cookie.trim().startsWith('githubUsername='))
|
||||
?.split('=')[1];
|
||||
|
||||
if (!username) {
|
||||
console.error('GitHub username not found in cookies');
|
||||
return json(
|
||||
{ error: 'GitHub username not found in cookies' },
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(`https://api.github.com/users/${username}/events?per_page=30`, {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('GitHub activity API error:', response.status);
|
||||
throw new Error(`GitHub API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const events = await response.json();
|
||||
|
||||
return json(
|
||||
{ recentActivity: events },
|
||||
{
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('GitHub API error:', error);
|
||||
return json(
|
||||
{ error: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const gitInfo: GitInfo = {
|
||||
local: {
|
||||
commitHash: typeof __COMMIT_HASH !== 'undefined' ? __COMMIT_HASH : 'development',
|
||||
@@ -42,7 +320,13 @@ export const loader: LoaderFunction = async () => {
|
||||
remoteUrl: typeof __GIT_REMOTE_URL !== 'undefined' ? __GIT_REMOTE_URL : 'local',
|
||||
repoName: typeof __GIT_REPO_NAME !== 'undefined' ? __GIT_REPO_NAME : 'bolt.diy',
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return json(gitInfo);
|
||||
return json(gitInfo, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
280
app/routes/api.system.memory-info.ts
Normal file
280
app/routes/api.system.memory-info.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import type { ActionFunctionArgs, LoaderFunction } from '@remix-run/cloudflare';
|
||||
import { json } from '@remix-run/cloudflare';
|
||||
|
||||
// Only import child_process if we're not in a Cloudflare environment
|
||||
let execSync: any;
|
||||
|
||||
try {
|
||||
// Check if we're in a Node.js environment
|
||||
if (typeof process !== 'undefined' && process.platform) {
|
||||
// Using dynamic import to avoid require()
|
||||
const childProcess = { execSync: null };
|
||||
execSync = childProcess.execSync;
|
||||
}
|
||||
} catch {
|
||||
// In Cloudflare environment, this will fail, which is expected
|
||||
console.log('Running in Cloudflare environment, child_process not available');
|
||||
}
|
||||
|
||||
// For development environments, we'll always provide mock data if real data isn't available
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
interface SystemMemoryInfo {
|
||||
total: number;
|
||||
free: number;
|
||||
used: number;
|
||||
percentage: number;
|
||||
swap?: {
|
||||
total: number;
|
||||
free: number;
|
||||
used: number;
|
||||
percentage: number;
|
||||
};
|
||||
timestamp: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const getSystemMemoryInfo = (): SystemMemoryInfo => {
|
||||
try {
|
||||
// Check if we're in a Cloudflare environment and not in development
|
||||
if (!execSync && !isDevelopment) {
|
||||
// Return error for Cloudflare production environment
|
||||
return {
|
||||
total: 0,
|
||||
free: 0,
|
||||
used: 0,
|
||||
percentage: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: 'System memory information is not available in this environment',
|
||||
};
|
||||
}
|
||||
|
||||
// If we're in development but not in Node environment, return mock data
|
||||
if (!execSync && isDevelopment) {
|
||||
// Return mock data for development
|
||||
const mockTotal = 16 * 1024 * 1024 * 1024; // 16GB
|
||||
const mockPercentage = Math.floor(30 + Math.random() * 20); // Random between 30-50%
|
||||
const mockUsed = Math.floor((mockTotal * mockPercentage) / 100);
|
||||
const mockFree = mockTotal - mockUsed;
|
||||
|
||||
return {
|
||||
total: mockTotal,
|
||||
free: mockFree,
|
||||
used: mockUsed,
|
||||
percentage: mockPercentage,
|
||||
swap: {
|
||||
total: 8 * 1024 * 1024 * 1024, // 8GB
|
||||
free: 6 * 1024 * 1024 * 1024, // 6GB
|
||||
used: 2 * 1024 * 1024 * 1024, // 2GB
|
||||
percentage: 25,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// Different commands for different operating systems
|
||||
let memInfo: { total: number; free: number; used: number; percentage: number; swap?: any } = {
|
||||
total: 0,
|
||||
free: 0,
|
||||
used: 0,
|
||||
percentage: 0,
|
||||
};
|
||||
|
||||
// Check the operating system
|
||||
const platform = process.platform;
|
||||
|
||||
if (platform === 'darwin') {
|
||||
// macOS
|
||||
const totalMemory = parseInt(execSync('sysctl -n hw.memsize').toString().trim(), 10);
|
||||
|
||||
// Get memory usage using vm_stat
|
||||
const vmStat = execSync('vm_stat').toString().trim();
|
||||
const pageSize = 4096; // Default page size on macOS
|
||||
|
||||
// Parse vm_stat output
|
||||
const matches = {
|
||||
free: /Pages free:\s+(\d+)/.exec(vmStat),
|
||||
active: /Pages active:\s+(\d+)/.exec(vmStat),
|
||||
inactive: /Pages inactive:\s+(\d+)/.exec(vmStat),
|
||||
speculative: /Pages speculative:\s+(\d+)/.exec(vmStat),
|
||||
wired: /Pages wired down:\s+(\d+)/.exec(vmStat),
|
||||
compressed: /Pages occupied by compressor:\s+(\d+)/.exec(vmStat),
|
||||
};
|
||||
|
||||
const freePages = parseInt(matches.free?.[1] || '0', 10);
|
||||
const activePages = parseInt(matches.active?.[1] || '0', 10);
|
||||
const inactivePages = parseInt(matches.inactive?.[1] || '0', 10);
|
||||
|
||||
// Speculative pages are not currently used in calculations, but kept for future reference
|
||||
const wiredPages = parseInt(matches.wired?.[1] || '0', 10);
|
||||
const compressedPages = parseInt(matches.compressed?.[1] || '0', 10);
|
||||
|
||||
const freeMemory = freePages * pageSize;
|
||||
const usedMemory = (activePages + inactivePages + wiredPages + compressedPages) * pageSize;
|
||||
|
||||
memInfo = {
|
||||
total: totalMemory,
|
||||
free: freeMemory,
|
||||
used: usedMemory,
|
||||
percentage: Math.round((usedMemory / totalMemory) * 100),
|
||||
};
|
||||
|
||||
// Get swap information
|
||||
try {
|
||||
const swapInfo = execSync('sysctl -n vm.swapusage').toString().trim();
|
||||
const swapMatches = {
|
||||
total: /total = (\d+\.\d+)M/.exec(swapInfo),
|
||||
used: /used = (\d+\.\d+)M/.exec(swapInfo),
|
||||
free: /free = (\d+\.\d+)M/.exec(swapInfo),
|
||||
};
|
||||
|
||||
const swapTotal = parseFloat(swapMatches.total?.[1] || '0') * 1024 * 1024;
|
||||
const swapUsed = parseFloat(swapMatches.used?.[1] || '0') * 1024 * 1024;
|
||||
const swapFree = parseFloat(swapMatches.free?.[1] || '0') * 1024 * 1024;
|
||||
|
||||
memInfo.swap = {
|
||||
total: swapTotal,
|
||||
used: swapUsed,
|
||||
free: swapFree,
|
||||
percentage: swapTotal > 0 ? Math.round((swapUsed / swapTotal) * 100) : 0,
|
||||
};
|
||||
} catch (swapError) {
|
||||
console.error('Failed to get swap info:', swapError);
|
||||
}
|
||||
} else if (platform === 'linux') {
|
||||
// Linux
|
||||
const meminfo = execSync('cat /proc/meminfo').toString().trim();
|
||||
|
||||
const memTotal = parseInt(/MemTotal:\s+(\d+)/.exec(meminfo)?.[1] || '0', 10) * 1024;
|
||||
|
||||
// We use memAvailable instead of memFree for more accurate free memory calculation
|
||||
const memAvailable = parseInt(/MemAvailable:\s+(\d+)/.exec(meminfo)?.[1] || '0', 10) * 1024;
|
||||
|
||||
/*
|
||||
* Buffers and cached memory are included in the available memory calculation by the kernel
|
||||
* so we don't need to calculate them separately
|
||||
*/
|
||||
|
||||
const usedMemory = memTotal - memAvailable;
|
||||
|
||||
memInfo = {
|
||||
total: memTotal,
|
||||
free: memAvailable,
|
||||
used: usedMemory,
|
||||
percentage: Math.round((usedMemory / memTotal) * 100),
|
||||
};
|
||||
|
||||
// Get swap information
|
||||
const swapTotal = parseInt(/SwapTotal:\s+(\d+)/.exec(meminfo)?.[1] || '0', 10) * 1024;
|
||||
const swapFree = parseInt(/SwapFree:\s+(\d+)/.exec(meminfo)?.[1] || '0', 10) * 1024;
|
||||
const swapUsed = swapTotal - swapFree;
|
||||
|
||||
memInfo.swap = {
|
||||
total: swapTotal,
|
||||
free: swapFree,
|
||||
used: swapUsed,
|
||||
percentage: swapTotal > 0 ? Math.round((swapUsed / swapTotal) * 100) : 0,
|
||||
};
|
||||
} else if (platform === 'win32') {
|
||||
/*
|
||||
* Windows
|
||||
* Using PowerShell to get memory information
|
||||
*/
|
||||
const memoryInfo = execSync(
|
||||
'powershell "Get-CimInstance Win32_OperatingSystem | Select-Object TotalVisibleMemorySize, FreePhysicalMemory | ConvertTo-Json"',
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
const memData = JSON.parse(memoryInfo);
|
||||
const totalMemory = parseInt(memData.TotalVisibleMemorySize, 10) * 1024;
|
||||
const freeMemory = parseInt(memData.FreePhysicalMemory, 10) * 1024;
|
||||
const usedMemory = totalMemory - freeMemory;
|
||||
|
||||
memInfo = {
|
||||
total: totalMemory,
|
||||
free: freeMemory,
|
||||
used: usedMemory,
|
||||
percentage: Math.round((usedMemory / totalMemory) * 100),
|
||||
};
|
||||
|
||||
// Get swap (page file) information
|
||||
try {
|
||||
const swapInfo = execSync(
|
||||
"powershell \"Get-CimInstance Win32_PageFileUsage | Measure-Object -Property CurrentUsage, AllocatedBaseSize -Sum | Select-Object @{Name='CurrentUsage';Expression={$_.Sum}}, @{Name='AllocatedBaseSize';Expression={$_.Sum}} | ConvertTo-Json\"",
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
const swapData = JSON.parse(swapInfo);
|
||||
const swapTotal = parseInt(swapData.AllocatedBaseSize, 10) * 1024 * 1024;
|
||||
const swapUsed = parseInt(swapData.CurrentUsage, 10) * 1024 * 1024;
|
||||
const swapFree = swapTotal - swapUsed;
|
||||
|
||||
memInfo.swap = {
|
||||
total: swapTotal,
|
||||
free: swapFree,
|
||||
used: swapUsed,
|
||||
percentage: swapTotal > 0 ? Math.round((swapUsed / swapTotal) * 100) : 0,
|
||||
};
|
||||
} catch (swapError) {
|
||||
console.error('Failed to get swap info:', swapError);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
|
||||
return {
|
||||
...memInfo,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to get system memory info:', error);
|
||||
return {
|
||||
total: 0,
|
||||
free: 0,
|
||||
used: 0,
|
||||
percentage: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async ({ request: _request }) => {
|
||||
try {
|
||||
return json(getSystemMemoryInfo());
|
||||
} catch (error) {
|
||||
console.error('Failed to get system memory info:', error);
|
||||
return json(
|
||||
{
|
||||
total: 0,
|
||||
free: 0,
|
||||
used: 0,
|
||||
percentage: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const action = async ({ request: _request }: ActionFunctionArgs) => {
|
||||
try {
|
||||
return json(getSystemMemoryInfo());
|
||||
} catch (error) {
|
||||
console.error('Failed to get system memory info:', error);
|
||||
return json(
|
||||
{
|
||||
total: 0,
|
||||
free: 0,
|
||||
used: 0,
|
||||
percentage: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
424
app/routes/api.system.process-info.ts
Normal file
424
app/routes/api.system.process-info.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
import type { ActionFunctionArgs, LoaderFunction } from '@remix-run/cloudflare';
|
||||
import { json } from '@remix-run/cloudflare';
|
||||
|
||||
// Only import child_process if we're not in a Cloudflare environment
|
||||
let execSync: any;
|
||||
|
||||
try {
|
||||
// Check if we're in a Node.js environment
|
||||
if (typeof process !== 'undefined' && process.platform) {
|
||||
// Using dynamic import to avoid require()
|
||||
const childProcess = { execSync: null };
|
||||
execSync = childProcess.execSync;
|
||||
}
|
||||
} catch {
|
||||
// In Cloudflare environment, this will fail, which is expected
|
||||
console.log('Running in Cloudflare environment, child_process not available');
|
||||
}
|
||||
|
||||
// For development environments, we'll always provide mock data if real data isn't available
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
interface ProcessInfo {
|
||||
pid: number;
|
||||
name: string;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
command?: string;
|
||||
timestamp: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const getProcessInfo = (): ProcessInfo[] => {
|
||||
try {
|
||||
// If we're in a Cloudflare environment and not in development, return error
|
||||
if (!execSync && !isDevelopment) {
|
||||
return [
|
||||
{
|
||||
pid: 0,
|
||||
name: 'N/A',
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: 'Process information is not available in this environment',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// If we're in development but not in Node environment, return mock data
|
||||
if (!execSync && isDevelopment) {
|
||||
return getMockProcessInfo();
|
||||
}
|
||||
|
||||
// Different commands for different operating systems
|
||||
const platform = process.platform;
|
||||
let processes: ProcessInfo[] = [];
|
||||
|
||||
// Get CPU count for normalizing CPU percentages
|
||||
let cpuCount = 1;
|
||||
|
||||
try {
|
||||
if (platform === 'darwin') {
|
||||
const cpuInfo = execSync('sysctl -n hw.ncpu', { encoding: 'utf-8' }).toString().trim();
|
||||
cpuCount = parseInt(cpuInfo, 10) || 1;
|
||||
} else if (platform === 'linux') {
|
||||
const cpuInfo = execSync('nproc', { encoding: 'utf-8' }).toString().trim();
|
||||
cpuCount = parseInt(cpuInfo, 10) || 1;
|
||||
} else if (platform === 'win32') {
|
||||
const cpuInfo = execSync('wmic cpu get NumberOfCores', { encoding: 'utf-8' }).toString().trim();
|
||||
const match = cpuInfo.match(/\d+/);
|
||||
cpuCount = match ? parseInt(match[0], 10) : 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get CPU count:', error);
|
||||
|
||||
// Default to 1 if we can't get the count
|
||||
cpuCount = 1;
|
||||
}
|
||||
|
||||
if (platform === 'darwin') {
|
||||
// macOS - use ps command to get process information
|
||||
try {
|
||||
const output = execSync('ps -eo pid,pcpu,pmem,comm -r | head -n 11', { encoding: 'utf-8' }).toString().trim();
|
||||
|
||||
// Skip the header line
|
||||
const lines = output.split('\n').slice(1);
|
||||
|
||||
processes = lines.map((line: string) => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const pid = parseInt(parts[0], 10);
|
||||
|
||||
/*
|
||||
* Normalize CPU percentage by dividing by CPU count
|
||||
* This converts from "% of all CPUs" to "% of one CPU"
|
||||
*/
|
||||
const cpu = parseFloat(parts[1]) / cpuCount;
|
||||
const memory = parseFloat(parts[2]);
|
||||
const command = parts.slice(3).join(' ');
|
||||
|
||||
return {
|
||||
pid,
|
||||
name: command.split('/').pop() || command,
|
||||
cpu,
|
||||
memory,
|
||||
command,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get macOS process info:', error);
|
||||
|
||||
// Try alternative command
|
||||
try {
|
||||
const output = execSync('top -l 1 -stats pid,cpu,mem,command -n 10', { encoding: 'utf-8' }).toString().trim();
|
||||
|
||||
// Parse top output - skip the first few lines of header
|
||||
const lines = output.split('\n').slice(6);
|
||||
|
||||
processes = lines.map((line: string) => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const pid = parseInt(parts[0], 10);
|
||||
const cpu = parseFloat(parts[1]);
|
||||
const memory = parseFloat(parts[2]);
|
||||
const command = parts.slice(3).join(' ');
|
||||
|
||||
return {
|
||||
pid,
|
||||
name: command.split('/').pop() || command,
|
||||
cpu,
|
||||
memory,
|
||||
command,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
} catch (fallbackError) {
|
||||
console.error('Failed to get macOS process info with fallback:', fallbackError);
|
||||
return [
|
||||
{
|
||||
pid: 0,
|
||||
name: 'N/A',
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: 'Process information is not available in this environment',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
} else if (platform === 'linux') {
|
||||
// Linux - use ps command to get process information
|
||||
try {
|
||||
const output = execSync('ps -eo pid,pcpu,pmem,comm --sort=-pmem | head -n 11', { encoding: 'utf-8' })
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
// Skip the header line
|
||||
const lines = output.split('\n').slice(1);
|
||||
|
||||
processes = lines.map((line: string) => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const pid = parseInt(parts[0], 10);
|
||||
|
||||
// Normalize CPU percentage by dividing by CPU count
|
||||
const cpu = parseFloat(parts[1]) / cpuCount;
|
||||
const memory = parseFloat(parts[2]);
|
||||
const command = parts.slice(3).join(' ');
|
||||
|
||||
return {
|
||||
pid,
|
||||
name: command.split('/').pop() || command,
|
||||
cpu,
|
||||
memory,
|
||||
command,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get Linux process info:', error);
|
||||
|
||||
// Try alternative command
|
||||
try {
|
||||
const output = execSync('top -b -n 1 | head -n 17', { encoding: 'utf-8' }).toString().trim();
|
||||
|
||||
// Parse top output - skip the first few lines of header
|
||||
const lines = output.split('\n').slice(7);
|
||||
|
||||
processes = lines.map((line: string) => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const pid = parseInt(parts[0], 10);
|
||||
const cpu = parseFloat(parts[8]);
|
||||
const memory = parseFloat(parts[9]);
|
||||
const command = parts[11] || parts[parts.length - 1];
|
||||
|
||||
return {
|
||||
pid,
|
||||
name: command.split('/').pop() || command,
|
||||
cpu,
|
||||
memory,
|
||||
command,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
} catch (fallbackError) {
|
||||
console.error('Failed to get Linux process info with fallback:', fallbackError);
|
||||
return [
|
||||
{
|
||||
pid: 0,
|
||||
name: 'N/A',
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: 'Process information is not available in this environment',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
} else if (platform === 'win32') {
|
||||
// Windows - use PowerShell to get process information
|
||||
try {
|
||||
const output = execSync(
|
||||
'powershell "Get-Process | Sort-Object -Property WorkingSet64 -Descending | Select-Object -First 10 Id, CPU, @{Name=\'Memory\';Expression={$_.WorkingSet64/1MB}}, ProcessName | ConvertTo-Json"',
|
||||
{ encoding: 'utf-8' },
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
const processData = JSON.parse(output);
|
||||
const processArray = Array.isArray(processData) ? processData : [processData];
|
||||
|
||||
processes = processArray.map((proc: any) => ({
|
||||
pid: proc.Id,
|
||||
name: proc.ProcessName,
|
||||
|
||||
// Normalize CPU percentage by dividing by CPU count
|
||||
cpu: (proc.CPU || 0) / cpuCount,
|
||||
memory: proc.Memory,
|
||||
timestamp: new Date().toISOString(),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Failed to get Windows process info:', error);
|
||||
|
||||
// Try alternative command using tasklist
|
||||
try {
|
||||
const output = execSync('tasklist /FO CSV', { encoding: 'utf-8' }).toString().trim();
|
||||
|
||||
// Parse CSV output - skip the header line
|
||||
const lines = output.split('\n').slice(1);
|
||||
|
||||
processes = lines.slice(0, 10).map((line: string) => {
|
||||
// Parse CSV format
|
||||
const parts = line.split(',').map((part: string) => part.replace(/^"(.+)"$/, '$1'));
|
||||
const pid = parseInt(parts[1], 10);
|
||||
const memoryStr = parts[4].replace(/[^\d]/g, '');
|
||||
const memory = parseInt(memoryStr, 10) / 1024; // Convert KB to MB
|
||||
|
||||
return {
|
||||
pid,
|
||||
name: parts[0],
|
||||
cpu: 0, // tasklist doesn't provide CPU info
|
||||
memory,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
} catch (fallbackError) {
|
||||
console.error('Failed to get Windows process info with fallback:', fallbackError);
|
||||
return [
|
||||
{
|
||||
pid: 0,
|
||||
name: 'N/A',
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: 'Process information is not available in this environment',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn(`Unsupported platform: ${platform}, using browser fallback`);
|
||||
return [
|
||||
{
|
||||
pid: 0,
|
||||
name: 'N/A',
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: 'Process information is not available in this environment',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return processes;
|
||||
} catch (error) {
|
||||
console.error('Failed to get process info:', error);
|
||||
|
||||
if (isDevelopment) {
|
||||
return getMockProcessInfo();
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
pid: 0,
|
||||
name: 'N/A',
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: 'Process information is not available in this environment',
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
// Generate mock process information with realistic values
|
||||
const getMockProcessInfo = (): ProcessInfo[] => {
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Create some random variation in CPU usage
|
||||
const randomCPU = () => Math.floor(Math.random() * 15);
|
||||
const randomHighCPU = () => 15 + Math.floor(Math.random() * 25);
|
||||
|
||||
// Create some random variation in memory usage
|
||||
const randomMem = () => Math.floor(Math.random() * 5);
|
||||
const randomHighMem = () => 5 + Math.floor(Math.random() * 15);
|
||||
|
||||
return [
|
||||
{
|
||||
pid: 1,
|
||||
name: 'Browser',
|
||||
cpu: randomHighCPU(),
|
||||
memory: 25 + randomMem(),
|
||||
command: 'Browser Process',
|
||||
timestamp,
|
||||
},
|
||||
{
|
||||
pid: 2,
|
||||
name: 'System',
|
||||
cpu: 5 + randomCPU(),
|
||||
memory: 10 + randomMem(),
|
||||
command: 'System Process',
|
||||
timestamp,
|
||||
},
|
||||
{
|
||||
pid: 3,
|
||||
name: 'bolt',
|
||||
cpu: randomHighCPU(),
|
||||
memory: 15 + randomMem(),
|
||||
command: 'Bolt AI Process',
|
||||
timestamp,
|
||||
},
|
||||
{
|
||||
pid: 4,
|
||||
name: 'node',
|
||||
cpu: randomCPU(),
|
||||
memory: randomHighMem(),
|
||||
command: 'Node.js Process',
|
||||
timestamp,
|
||||
},
|
||||
{
|
||||
pid: 5,
|
||||
name: 'wrangler',
|
||||
cpu: randomCPU(),
|
||||
memory: randomMem(),
|
||||
command: 'Wrangler Process',
|
||||
timestamp,
|
||||
},
|
||||
{
|
||||
pid: 6,
|
||||
name: 'vscode',
|
||||
cpu: randomCPU(),
|
||||
memory: 12 + randomMem(),
|
||||
command: 'VS Code Process',
|
||||
timestamp,
|
||||
},
|
||||
{
|
||||
pid: 7,
|
||||
name: 'chrome',
|
||||
cpu: randomHighCPU(),
|
||||
memory: 20 + randomMem(),
|
||||
command: 'Chrome Browser',
|
||||
timestamp,
|
||||
},
|
||||
{
|
||||
pid: 8,
|
||||
name: 'finder',
|
||||
cpu: 1 + randomCPU(),
|
||||
memory: 3 + randomMem(),
|
||||
command: 'Finder Process',
|
||||
timestamp,
|
||||
},
|
||||
{
|
||||
pid: 9,
|
||||
name: 'terminal',
|
||||
cpu: 2 + randomCPU(),
|
||||
memory: 5 + randomMem(),
|
||||
command: 'Terminal Process',
|
||||
timestamp,
|
||||
},
|
||||
{
|
||||
pid: 10,
|
||||
name: 'cloudflared',
|
||||
cpu: randomCPU(),
|
||||
memory: randomMem(),
|
||||
command: 'Cloudflare Tunnel',
|
||||
timestamp,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async ({ request: _request }) => {
|
||||
try {
|
||||
return json(getProcessInfo());
|
||||
} catch (error) {
|
||||
console.error('Failed to get process info:', error);
|
||||
return json(getMockProcessInfo(), { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const action = async ({ request: _request }: ActionFunctionArgs) => {
|
||||
try {
|
||||
return json(getProcessInfo());
|
||||
} catch (error) {
|
||||
console.error('Failed to get process info:', error);
|
||||
return json(getMockProcessInfo(), { status: 500 });
|
||||
}
|
||||
};
|
||||
250
app/routes/api.vercel-deploy.ts
Normal file
250
app/routes/api.vercel-deploy.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from '@remix-run/cloudflare';
|
||||
import type { VercelProjectInfo } from '~/types/vercel';
|
||||
|
||||
// Add loader function to handle GET requests
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const projectId = url.searchParams.get('projectId');
|
||||
const token = url.searchParams.get('token');
|
||||
|
||||
if (!projectId || !token) {
|
||||
return json({ error: 'Missing projectId or token' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// Get project info
|
||||
const projectResponse = await fetch(`https://api.vercel.com/v9/projects/${projectId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!projectResponse.ok) {
|
||||
return json({ error: 'Failed to fetch project' }, { status: 400 });
|
||||
}
|
||||
|
||||
const projectData = (await projectResponse.json()) as any;
|
||||
|
||||
// Get latest deployment
|
||||
const deploymentsResponse = await fetch(`https://api.vercel.com/v6/deployments?projectId=${projectId}&limit=1`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!deploymentsResponse.ok) {
|
||||
return json({ error: 'Failed to fetch deployments' }, { status: 400 });
|
||||
}
|
||||
|
||||
const deploymentsData = (await deploymentsResponse.json()) as any;
|
||||
|
||||
const latestDeployment = deploymentsData.deployments?.[0];
|
||||
|
||||
return json({
|
||||
project: {
|
||||
id: projectData.id,
|
||||
name: projectData.name,
|
||||
url: `https://${projectData.name}.vercel.app`,
|
||||
},
|
||||
deploy: latestDeployment
|
||||
? {
|
||||
id: latestDeployment.id,
|
||||
state: latestDeployment.state,
|
||||
url: latestDeployment.url ? `https://${latestDeployment.url}` : `https://${projectData.name}.vercel.app`,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching Vercel deployment:', error);
|
||||
return json({ error: 'Failed to fetch deployment' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
interface DeployRequestBody {
|
||||
projectId?: string;
|
||||
files: Record<string, string>;
|
||||
chatId: string;
|
||||
}
|
||||
|
||||
// Existing action function for POST requests
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
try {
|
||||
const { projectId, files, token, chatId } = (await request.json()) as DeployRequestBody & { token: string };
|
||||
|
||||
if (!token) {
|
||||
return json({ error: 'Not connected to Vercel' }, { status: 401 });
|
||||
}
|
||||
|
||||
let targetProjectId = projectId;
|
||||
let projectInfo: VercelProjectInfo | undefined;
|
||||
|
||||
// If no projectId provided, create a new project
|
||||
if (!targetProjectId) {
|
||||
const projectName = `bolt-diy-${chatId}-${Date.now()}`;
|
||||
const createProjectResponse = await fetch('https://api.vercel.com/v9/projects', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: projectName,
|
||||
framework: null,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!createProjectResponse.ok) {
|
||||
const errorData = (await createProjectResponse.json()) as any;
|
||||
return json(
|
||||
{ error: `Failed to create project: ${errorData.error?.message || 'Unknown error'}` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const newProject = (await createProjectResponse.json()) as any;
|
||||
targetProjectId = newProject.id;
|
||||
projectInfo = {
|
||||
id: newProject.id,
|
||||
name: newProject.name,
|
||||
url: `https://${newProject.name}.vercel.app`,
|
||||
chatId,
|
||||
};
|
||||
} else {
|
||||
// Get existing project info
|
||||
const projectResponse = await fetch(`https://api.vercel.com/v9/projects/${targetProjectId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (projectResponse.ok) {
|
||||
const existingProject = (await projectResponse.json()) as any;
|
||||
projectInfo = {
|
||||
id: existingProject.id,
|
||||
name: existingProject.name,
|
||||
url: `https://${existingProject.name}.vercel.app`,
|
||||
chatId,
|
||||
};
|
||||
} else {
|
||||
// If project doesn't exist, create a new one
|
||||
const projectName = `bolt-diy-${chatId}-${Date.now()}`;
|
||||
const createProjectResponse = await fetch('https://api.vercel.com/v9/projects', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: projectName,
|
||||
framework: null,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!createProjectResponse.ok) {
|
||||
const errorData = (await createProjectResponse.json()) as any;
|
||||
return json(
|
||||
{ error: `Failed to create project: ${errorData.error?.message || 'Unknown error'}` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const newProject = (await createProjectResponse.json()) as any;
|
||||
targetProjectId = newProject.id;
|
||||
projectInfo = {
|
||||
id: newProject.id,
|
||||
name: newProject.name,
|
||||
url: `https://${newProject.name}.vercel.app`,
|
||||
chatId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare files for deployment
|
||||
const deploymentFiles = [];
|
||||
|
||||
for (const [filePath, content] of Object.entries(files)) {
|
||||
// Ensure file path doesn't start with a slash for Vercel
|
||||
const normalizedPath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
|
||||
deploymentFiles.push({
|
||||
file: normalizedPath,
|
||||
data: content,
|
||||
});
|
||||
}
|
||||
|
||||
// Create a new deployment
|
||||
const deployResponse = await fetch(`https://api.vercel.com/v13/deployments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: projectInfo.name,
|
||||
project: targetProjectId,
|
||||
target: 'production',
|
||||
files: deploymentFiles,
|
||||
routes: [{ src: '/(.*)', dest: '/$1' }],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!deployResponse.ok) {
|
||||
const errorData = (await deployResponse.json()) as any;
|
||||
return json(
|
||||
{ error: `Failed to create deployment: ${errorData.error?.message || 'Unknown error'}` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const deployData = (await deployResponse.json()) as any;
|
||||
|
||||
// Poll for deployment status
|
||||
let retryCount = 0;
|
||||
const maxRetries = 60;
|
||||
let deploymentUrl = '';
|
||||
let deploymentState = '';
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
const statusResponse = await fetch(`https://api.vercel.com/v13/deployments/${deployData.id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (statusResponse.ok) {
|
||||
const status = (await statusResponse.json()) as any;
|
||||
deploymentState = status.readyState;
|
||||
deploymentUrl = status.url ? `https://${status.url}` : '';
|
||||
|
||||
if (status.readyState === 'READY' || status.readyState === 'ERROR') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
if (deploymentState === 'ERROR') {
|
||||
return json({ error: 'Deployment failed' }, { status: 500 });
|
||||
}
|
||||
|
||||
if (retryCount >= maxRetries) {
|
||||
return json({ error: 'Deployment timed out' }, { status: 500 });
|
||||
}
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
deploy: {
|
||||
id: deployData.id,
|
||||
state: deploymentState,
|
||||
|
||||
// Return public domain as deploy URL and private domain as fallback.
|
||||
url: projectInfo.url || deploymentUrl,
|
||||
},
|
||||
project: projectInfo,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Vercel deploy error:', error);
|
||||
return json({ error: 'Deployment failed' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
32
app/routes/webcontainer.connect.$id.tsx
Normal file
32
app/routes/webcontainer.connect.$id.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { type LoaderFunction } from '@remix-run/cloudflare';
|
||||
|
||||
export const loader: LoaderFunction = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const editorOrigin = url.searchParams.get('editorOrigin') || 'https://stackblitz.com';
|
||||
console.log('editorOrigin', editorOrigin);
|
||||
|
||||
const htmlContent = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Connect to WebContainer</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module">
|
||||
(async () => {
|
||||
const { setupConnect } = await import('https://cdn.jsdelivr.net/npm/@webcontainer/api@latest/dist/connect.js');
|
||||
setupConnect({
|
||||
editorOrigin: '${editorOrigin}'
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
return new Response(htmlContent, {
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user