mirror of
https://github.com/yamadashy/repomix.git
synced 2025-06-11 00:25:54 +03:00
feat(website): Run repomix in quiet mode on the website
This commit is contained in:
@@ -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
|
||||
|
||||
1587
website/server/package-lock.json
generated
1587
website/server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
175
website/server/src/utils/logger.ts
Normal file
175
website/server/src/utils/logger.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
14
website/server/src/utils/network.ts
Normal file
14
website/server/src/utils/network.ts
Normal 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'
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user