feat(website): Run repomix in quiet mode on the website

This commit is contained in:
Yamada Dev
2025-02-12 00:31:29 +09:00
committed by yamadashy
parent 2b894736be
commit 10820407c7
8 changed files with 1840 additions and 44 deletions

View File

@@ -28,3 +28,8 @@ services:
- PORT=8080
# override default command
command: sh -c "npm i && npm run dev"
develop:
watch:
- action: sync+restart
path: ./server
target: /app

File diff suppressed because it is too large Load Diff

View File

@@ -9,11 +9,13 @@
"cloud-deploy": "gcloud builds submit --config=cloudbuild.yaml ."
},
"dependencies": {
"@google-cloud/logging-winston": "^6.0.0",
"@hono/node-server": "^1.13.8",
"adm-zip": "^0.5.16",
"hono": "^4.6.20",
"pako": "^2.1.0",
"repomix": "^0.2.26",
"repomix": "^0.2.29",
"winston": "^3.17.0",
"zod": "^3.24.1"
},
"devDependencies": {

View File

@@ -3,21 +3,29 @@ import { Hono } from 'hono';
import { bodyLimit } from 'hono/body-limit';
import { compress } from 'hono/compress';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { timeout } from 'hono/timeout';
import { setLogLevel } from 'repomix';
import { FILE_SIZE_LIMITS } from './constants.js';
import { processZipFile } from './processZipFile.js';
import { processRemoteRepo } from './remoteRepo.js';
import type { ErrorResponse, PackResult } from './types.js';
import { handlePackError } from './utils/errorHandler.js';
import { cloudLogger, createErrorResponse, formatLatency, logError, logInfo } from './utils/logger.js';
import { getProcessConcurrency } from './utils/processConcurrency.js';
// Log metrics
console.log('Server Process concurrency:', getProcessConcurrency());
// Log server metrics on startup
logInfo('Server starting', {
metrics: {
processConcurrency: getProcessConcurrency(),
},
});
const app = new Hono();
// Set up CORS
// Setup custom logger
app.use('*', cloudLogger());
// Configure CORS
app.use(
'/*',
cors({
@@ -29,83 +37,100 @@ app.use(
}),
);
// Enable compression for all routes
// Enable compression
app.use(compress());
// Set up timeout middleware for /api routes
// Set timeout for API routes
app.use('/api', timeout(30000));
// Set up logger middleware
app.use(logger());
// GET /health
// Health check endpoint
app.get('/health', (c) => c.text('OK'));
// POST /api/pack
// Main packing endpoint
app.post(
'/api/pack',
bodyLimit({
maxSize: FILE_SIZE_LIMITS.MAX_REQUEST_SIZE,
onError: (c) => {
return c.text('File size too large :(', 413);
const requestId = c.get('requestId');
const response = createErrorResponse('File size too large', requestId);
return c.json(response, 413);
},
}),
async (c) => {
try {
const formData = await c.req.formData();
const requestId = c.get('requestId');
// Get format and options from formData
// Get form data
const format = formData.get('format') as 'xml' | 'markdown' | 'plain';
const options = JSON.parse(formData.get('options') as string);
// Check if we have a file or URL
const file = formData.get('file') as File | null;
const url = formData.get('url') as string | null;
// Validate input
if (!file && !url) {
return c.json({ error: 'Either repository URL or file is required' } as ErrorResponse, 400);
return c.json(createErrorResponse('Either repository URL or file is required', requestId), 400);
}
if (!['xml', 'markdown', 'plain'].includes(format)) {
return c.json({ error: 'Invalid format specified' } as ErrorResponse, 400);
return c.json(createErrorResponse('Invalid format specified', requestId), 400);
}
// Get client IP address
// Get client IP
const clientIp =
c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || c.req.header('cf-connecting-ip') || '0.0.0.0';
c.req.header('x-forwarded-for')?.split(',')[0] ||
c.req.header('x-real-ip') ||
c.req.header('cf-connecting-ip') ||
'0.0.0.0';
const startTime = Date.now();
// Process file or repository
let result: PackResult;
if (file) {
result = await processZipFile(file, format, options, clientIp);
} else {
if (!url) {
return c.json({ error: 'Repository URL is required' } as ErrorResponse, 400);
return c.json(createErrorResponse('Repository URL is required', requestId), 400);
}
result = await processRemoteRepo(url, format, options, clientIp);
}
// Log operation result
logInfo('Pack operation completed', {
requestId,
format,
repository: result.metadata.repository,
latency: formatLatency(startTime),
metrics: {
totalFiles: result.metadata.summary?.totalFiles,
totalCharacters: result.metadata.summary?.totalCharacters,
totalTokens: result.metadata.summary?.totalTokens,
},
});
return c.json(result);
} catch (error) {
console.error('Error processing request:', error);
// Handle errors
logError('Pack operation failed', error instanceof Error ? error : new Error('Unknown error'), {
requestId: c.get('requestId'),
});
const appError = handlePackError(error);
return c.json(
{
error: appError.message,
} as ErrorResponse,
appError.statusCode,
);
return c.json(createErrorResponse(appError.message, c.get('requestId')), appError.statusCode);
}
},
);
// Start the server
// Start server
const port = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3000;
console.log(`Server is starting on port ${port}`);
logInfo(`Server starting on port ${port}`);
serve({
fetch: app.fetch,
port,
});
// Export the app for testing
// Export app for testing
export default app;

View File

@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
import fs from 'node:fs/promises';
import path from 'node:path';
import AdmZip from 'adm-zip';
import { type CliOptions, runDefaultAction } from 'repomix';
import { type CliOptions, runDefaultAction, setLogLevel } from 'repomix';
import { packRequestSchema } from './schemas/request.js';
import type { PackOptions, PackResult } from './types.js';
import { generateCacheKey } from './utils/cache.js';
@@ -79,8 +79,11 @@ export async function processZipFile(
topFilesLen: 10,
include: sanitizedIncludePatterns,
ignore: sanitizedIgnorePatterns,
quiet: true, // Enable quiet mode to suppress output
} as CliOptions;
setLogLevel(-1);
const tempDirPath = await createTempDirectory();
try {

View File

@@ -1,6 +1,6 @@
import { randomUUID } from 'node:crypto';
import fs from 'node:fs/promises';
import { type CliOptions, runRemoteAction } from 'repomix';
import { type CliOptions, runCli } from 'repomix';
import { packRequestSchema } from './schemas/request.js';
import type { PackOptions, PackResult } from './types.js';
import { generateCacheKey } from './utils/cache.js';
@@ -48,6 +48,7 @@ export async function processRemoteRepo(
// Create CLI options with correct mapping
const cliOptions = {
remote: repoUrl,
output: outputFilePath,
style: validatedData.format,
parsableStyle: validatedData.options.outputParsable,
@@ -60,11 +61,15 @@ export async function processRemoteRepo(
topFilesLen: 10,
include: sanitizedIncludePatterns,
ignore: sanitizedIgnorePatterns,
quiet: true, // Enable quiet mode to suppress output
} as CliOptions;
try {
// Execute remote action
const result = await runRemoteAction(repoUrl, cliOptions);
const result = await runCli(['.'], process.cwd(), cliOptions);
if (!result) {
throw new AppError('Remote action failed to return a result', 500);
}
const { packResult } = result;
// Read the generated file

View File

@@ -0,0 +1,175 @@
import { LoggingWinston } from '@google-cloud/logging-winston';
import type { Context, Next } from 'hono';
import winston from 'winston';
import { getClientIP } from './network.js';
// Augment Hono's context type
declare module 'hono' {
interface ContextVariableMap {
requestId: string;
}
}
// Configure transports based on environment
function createLogger() {
const transports: winston.transport[] = [
new winston.transports.Console({
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
}),
];
// Add Cloud Logging transport only in production
if (process.env.NODE_ENV === 'production') {
const loggingWinston = new LoggingWinston();
transports.push(loggingWinston);
}
return winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
transports,
});
}
// Create the logger instance
const logger = createLogger();
// Generate unique request identifier
function generateRequestId(): string {
return `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
// Calculate and format request latency
export function formatLatency(startTime: number): string {
const latencyMs = Date.now() - startTime;
const latencySec = latencyMs / 1000; // Convert to seconds
return `${latencySec.toFixed(3)}s`;
}
// Error response interface with request tracking
interface ErrorResponse {
error: string;
requestId: string;
timestamp: string;
}
// Generate standardized error response
export function createErrorResponse(message: string, requestId: string): ErrorResponse {
return {
error: message,
requestId,
timestamp: new Date().toISOString(),
};
}
// Main logging middleware for Hono
export function cloudLogger() {
return async function loggerMiddleware(c: Context, next: Next) {
const requestId = generateRequestId();
const startTime = Date.now();
// Add request ID to context for tracking
c.set('requestId', requestId);
// Collect basic request information
const method = c.req.method;
const url = new URL(c.req.url);
const userAgent = c.req.header('user-agent');
const referer = c.req.header('referer');
const remoteIp = getClientIP(c);
// Log request start
logger.info({
message: `${method} ${url.pathname} started`,
requestId,
httpRequest: {
requestMethod: method,
requestUrl: url.toString(),
userAgent,
referer,
remoteIp,
},
});
try {
// Process the request
await next();
// Collect response information
const status = c.res.status;
const latency = formatLatency(startTime);
const contentLength = Number.parseInt(c.res.headers.get('content-length') || '0', 10);
// Log successful response
logger.info({
message: `${method} ${url.pathname} completed`,
requestId,
httpRequest: {
requestMethod: method,
requestUrl: url.toString(),
status,
latency,
responseSize: contentLength,
userAgent,
referer,
remoteIp,
},
});
} catch (error) {
// Log error information
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error({
message: `${method} ${url.pathname} failed: ${errorMessage}`,
requestId,
error: {
message: errorMessage,
stack: error instanceof Error ? error.stack : undefined,
},
httpRequest: {
requestMethod: method,
requestUrl: url.toString(),
status: 500,
latency: formatLatency(startTime),
userAgent,
referer,
remoteIp,
},
});
throw error;
}
};
}
// Utility logging functions
export function logDebug(message: string, context?: Record<string, unknown>): void {
logger.debug({
message,
...context,
});
}
export function logInfo(message: string, context?: Record<string, unknown>): void {
logger.info({
message,
...context,
});
}
export function logWarning(message: string, context?: Record<string, unknown>): void {
logger.warn({
message,
...context,
});
}
export function logError(message: string, error?: Error, context?: Record<string, unknown>): void {
logger.error({
message,
error: error
? {
message: error.message,
stack: error.stack,
}
: undefined,
...context,
});
}

View File

@@ -0,0 +1,14 @@
import type { Context } from 'hono';
/**
* Get client IP address
* Handles various headers in Cloud Run environment
*/
export function getClientIP(c: Context): string {
return (
c.req.header('x-forwarded-for')?.split(',')[0] ||
c.req.header('x-real-ip') ||
c.req.header('cf-connecting-ip') ||
'0.0.0.0'
);
}