mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-16 01:12:46 +00:00
refactor: bare structure for new backend architecture and runtime; no behavior changes yet
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
// Load environment variables before other imports execute
|
||||
import './load-env.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const distEntrypoint = path.join(__dirname, 'dist', 'bootstrap.js');
|
||||
|
||||
const installMode = fs.existsSync(path.join(__dirname, '..', '.git')) ? 'git' : 'npm';
|
||||
|
||||
|
||||
2554
server/legacy-runtime.js
Normal file
2554
server/legacy-runtime.js
Normal file
File diff suppressed because it is too large
Load Diff
19
server/src/app.ts
Normal file
19
server/src/app.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { pathToFileURL } from 'url';
|
||||
|
||||
import { getRuntimePaths } from './config/runtime.js';
|
||||
import type { ServerApplication } from './shared/types/app.js';
|
||||
import { logger } from './shared/utils/logger.js';
|
||||
|
||||
export function createServerApplication(): ServerApplication {
|
||||
const runtimePaths = getRuntimePaths();
|
||||
|
||||
return {
|
||||
runtimePaths,
|
||||
start: async () => {
|
||||
logger.info('Bootstrapping backend via legacy runtime bridge', {
|
||||
legacyRuntime: runtimePaths.legacyRuntimePath,
|
||||
});
|
||||
await import(pathToFileURL(runtimePaths.legacyRuntimePath).href);
|
||||
},
|
||||
};
|
||||
}
|
||||
8
server/src/bootstrap.ts
Normal file
8
server/src/bootstrap.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createServerApplication } from './app.js';
|
||||
|
||||
async function startServerApplication(): Promise<void> {
|
||||
const application = createServerApplication();
|
||||
await application.start();
|
||||
}
|
||||
|
||||
await startServerApplication();
|
||||
20
server/src/config/runtime.ts
Normal file
20
server/src/config/runtime.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import type { RuntimePaths } from '../shared/types/app.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export function getRuntimePaths(): RuntimePaths {
|
||||
const serverSrcDir = path.resolve(__dirname, '..');
|
||||
const serverDir = path.resolve(serverSrcDir, '..');
|
||||
|
||||
return {
|
||||
serverSrcDir,
|
||||
serverDir,
|
||||
projectRoot: path.resolve(serverDir, '..'),
|
||||
legacyRuntimePath: path.join(serverDir, 'legacy-runtime.js'),
|
||||
bootstrapEntrypointPath: path.join(serverDir, 'dist', 'bootstrap.js'),
|
||||
};
|
||||
}
|
||||
1
server/src/modules/agent/.gitkeep
Normal file
1
server/src/modules/agent/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/auth/.gitkeep
Normal file
1
server/src/modules/auth/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/cli-auth/.gitkeep
Normal file
1
server/src/modules/cli-auth/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/files/.gitkeep
Normal file
1
server/src/modules/files/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/git/.gitkeep
Normal file
1
server/src/modules/git/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/projects/.gitkeep
Normal file
1
server/src/modules/projects/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/providers/claude/.gitkeep
Normal file
1
server/src/modules/providers/claude/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/providers/codex/.gitkeep
Normal file
1
server/src/modules/providers/codex/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/providers/cursor/.gitkeep
Normal file
1
server/src/modules/providers/cursor/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/providers/gemini/.gitkeep
Normal file
1
server/src/modules/providers/gemini/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/providers/mcp/.gitkeep
Normal file
1
server/src/modules/providers/mcp/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/providers/plugins/.gitkeep
Normal file
1
server/src/modules/providers/plugins/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/sessions/.gitkeep
Normal file
1
server/src/modules/sessions/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/settings/.gitkeep
Normal file
1
server/src/modules/settings/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/taskmaster/.gitkeep
Normal file
1
server/src/modules/taskmaster/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
server/src/modules/user/.gitkeep
Normal file
1
server/src/modules/user/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
21
server/src/shared/docs/openapi.ts
Normal file
21
server/src/shared/docs/openapi.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export type OpenApiPlan = {
|
||||
status: 'planned';
|
||||
description: string;
|
||||
nextSteps: string[];
|
||||
examples: Record<string, string>;
|
||||
};
|
||||
|
||||
export const openApiPlan: OpenApiPlan = {
|
||||
status: 'planned',
|
||||
description: 'Day 1 placeholder for the shared OpenAPI registry and document builder.',
|
||||
nextSteps: [
|
||||
'Register global tags for auth, projects, files, git, taskmaster, agent, and providers.',
|
||||
'Promote the endpoint inventory into explicit request and response schemas.',
|
||||
'Publish /api/openapi.json and Swagger UI once schemas are in place.',
|
||||
],
|
||||
examples: {
|
||||
authTag: 'Auth',
|
||||
projectsTag: 'Projects',
|
||||
providerTag: 'Providers',
|
||||
},
|
||||
};
|
||||
36
server/src/shared/http/api-response.ts
Normal file
36
server/src/shared/http/api-response.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { ApiErrorShape, ApiMeta, ApiSuccessShape } from '../types/http.js';
|
||||
|
||||
export function createApiMeta(requestId?: string, startedAt?: string): ApiMeta {
|
||||
return {
|
||||
requestId,
|
||||
startedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function createApiSuccessResponse<TData>(
|
||||
data: TData,
|
||||
meta?: ApiMeta
|
||||
): ApiSuccessShape<TData> {
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
meta,
|
||||
};
|
||||
}
|
||||
|
||||
export function createApiErrorResponse(
|
||||
code: string,
|
||||
message: string,
|
||||
meta?: ApiMeta,
|
||||
details?: unknown
|
||||
): ApiErrorShape {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code,
|
||||
message,
|
||||
details,
|
||||
},
|
||||
meta,
|
||||
};
|
||||
}
|
||||
9
server/src/shared/http/async-handler.ts
Normal file
9
server/src/shared/http/async-handler.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { NextFunction, Request, RequestHandler, Response } from 'express';
|
||||
|
||||
export function asyncHandler(
|
||||
handler: (req: Request, res: Response, next: NextFunction) => Promise<unknown>
|
||||
): RequestHandler {
|
||||
return (req, res, next) => {
|
||||
void Promise.resolve(handler(req, res, next)).catch(next);
|
||||
};
|
||||
}
|
||||
30
server/src/shared/http/error-handler.ts
Normal file
30
server/src/shared/http/error-handler.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
|
||||
import { AppError } from '../utils/app-error.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { createApiErrorResponse, createApiMeta } from './api-response.js';
|
||||
import { getRequestContext } from './request-context.js';
|
||||
|
||||
export function errorHandler(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
_next: NextFunction
|
||||
): void {
|
||||
const appError = error instanceof AppError ? error : new AppError(error.message);
|
||||
const context = getRequestContext(req);
|
||||
const payload = createApiErrorResponse(
|
||||
appError.code,
|
||||
appError.message,
|
||||
createApiMeta(context?.requestId, context?.startedAt),
|
||||
appError.details
|
||||
);
|
||||
|
||||
logger.error(appError.message, {
|
||||
code: appError.code,
|
||||
statusCode: appError.statusCode,
|
||||
requestId: context?.requestId,
|
||||
});
|
||||
|
||||
res.status(appError.statusCode).json(payload);
|
||||
}
|
||||
15
server/src/shared/http/not-found-handler.ts
Normal file
15
server/src/shared/http/not-found-handler.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
import { createApiErrorResponse, createApiMeta } from './api-response.js';
|
||||
import { getRequestContext } from './request-context.js';
|
||||
|
||||
export function notFoundHandler(req: Request, res: Response): void {
|
||||
const context = getRequestContext(req);
|
||||
const payload = createApiErrorResponse(
|
||||
'NOT_FOUND',
|
||||
`Route not found: ${req.originalUrl}`,
|
||||
createApiMeta(context?.requestId, context?.startedAt)
|
||||
);
|
||||
|
||||
res.status(404).json(payload);
|
||||
}
|
||||
27
server/src/shared/http/request-context.ts
Normal file
27
server/src/shared/http/request-context.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
|
||||
import type { RequestContext } from '../types/http.js';
|
||||
|
||||
type RequestWithContext = Request & {
|
||||
context?: RequestContext;
|
||||
};
|
||||
|
||||
export function getRequestContext(req: Request): RequestContext | undefined {
|
||||
return (req as RequestWithContext).context;
|
||||
}
|
||||
|
||||
// give every request a context with a unique ID and timestamp for tracking purposes
|
||||
export function requestContextMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
const requestId = randomUUID();
|
||||
const startedAt = new Date().toISOString();
|
||||
const context: RequestContext = {
|
||||
requestId,
|
||||
startedAt,
|
||||
};
|
||||
|
||||
(req as RequestWithContext).context = context;
|
||||
(res.locals as Record<string, unknown>).requestId = requestId;
|
||||
|
||||
next();
|
||||
}
|
||||
19
server/src/shared/types/app.ts
Normal file
19
server/src/shared/types/app.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { WebSocketServer } from 'ws';
|
||||
|
||||
export type RuntimePaths = {
|
||||
serverSrcDir: string;
|
||||
serverDir: string;
|
||||
projectRoot: string;
|
||||
legacyRuntimePath: string;
|
||||
bootstrapEntrypointPath: string;
|
||||
};
|
||||
|
||||
export type AppLocals = {
|
||||
requestId?: string;
|
||||
wss?: WebSocketServer;
|
||||
};
|
||||
|
||||
export type ServerApplication = {
|
||||
runtimePaths: RuntimePaths;
|
||||
start: () => Promise<void>;
|
||||
};
|
||||
71
server/src/shared/types/http.ts
Normal file
71
server/src/shared/types/http.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { Request } from 'express';
|
||||
|
||||
export type TransportKind = 'http' | 'sse' | 'ws';
|
||||
|
||||
/**
|
||||
* Meta information about an API response, such as request ID and timing details.
|
||||
* Different from RequestContext which is the internal server-side context for an
|
||||
* incoming request.
|
||||
*/
|
||||
export type ApiMeta = {
|
||||
requestId?: string;
|
||||
startedAt?: string;
|
||||
};
|
||||
|
||||
export type ApiSuccessShape<TData = unknown> = {
|
||||
success: true;
|
||||
data: TData;
|
||||
meta?: ApiMeta;
|
||||
};
|
||||
|
||||
export type ApiErrorShape = {
|
||||
success: false;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: unknown;
|
||||
};
|
||||
meta?: ApiMeta;
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal server-side context for an incoming request.
|
||||
* It's the source object. It's different from ApiMeta which is
|
||||
* meant for API responses.
|
||||
*/
|
||||
export type RequestContext = {
|
||||
requestId: string;
|
||||
startedAt: string;
|
||||
};
|
||||
|
||||
export type AuthenticatedUser = {
|
||||
id: number | string;
|
||||
username?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type AuthenticatedRequest = Request & {
|
||||
context?: RequestContext;
|
||||
user?: AuthenticatedUser;
|
||||
};
|
||||
|
||||
export type EndpointInventoryRecord = {
|
||||
transport: TransportKind;
|
||||
method: string;
|
||||
path: string;
|
||||
tag: string;
|
||||
authMode: string;
|
||||
sourceFile: string;
|
||||
sourceLine: number;
|
||||
purpose: string;
|
||||
consumerFiles: string[];
|
||||
inputs: {
|
||||
pathParams: string[];
|
||||
queryParams: string[];
|
||||
bodyHints: string[];
|
||||
};
|
||||
successShape: string;
|
||||
errorShape: string;
|
||||
sideEffects: string[];
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
};
|
||||
19
server/src/shared/utils/app-error.ts
Normal file
19
server/src/shared/utils/app-error.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export type AppErrorOptions = {
|
||||
code?: string;
|
||||
statusCode?: number;
|
||||
details?: unknown;
|
||||
};
|
||||
|
||||
export class AppError extends Error {
|
||||
readonly code: string;
|
||||
readonly statusCode: number;
|
||||
readonly details?: unknown;
|
||||
|
||||
constructor(message: string, options: AppErrorOptions = {}) {
|
||||
super(message);
|
||||
this.name = 'AppError';
|
||||
this.code = options.code ?? 'INTERNAL_ERROR';
|
||||
this.statusCode = options.statusCode ?? 500;
|
||||
this.details = options.details;
|
||||
}
|
||||
}
|
||||
32
server/src/shared/utils/logger.ts
Normal file
32
server/src/shared/utils/logger.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
type LoggerLevel = 'info' | 'warn' | 'error';
|
||||
|
||||
function formatMetadata(metadata?: Record<string, unknown>): string {
|
||||
if (!metadata || Object.keys(metadata).length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ` ${JSON.stringify(metadata)}`;
|
||||
}
|
||||
|
||||
function write(level: LoggerLevel, message: string, metadata?: Record<string, unknown>): void {
|
||||
const prefix = `[server:${level}]`;
|
||||
const formatted = `${prefix} ${message}${formatMetadata(metadata)}`;
|
||||
|
||||
if (level === 'error') {
|
||||
console.error(formatted);
|
||||
return;
|
||||
}
|
||||
|
||||
if (level === 'warn') {
|
||||
console.warn(formatted);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(formatted);
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
info: (message: string, metadata?: Record<string, unknown>) => write('info', message, metadata),
|
||||
warn: (message: string, metadata?: Record<string, unknown>) => write('warn', message, metadata),
|
||||
error: (message: string, metadata?: Record<string, unknown>) => write('error', message, metadata),
|
||||
};
|
||||
20
server/tsconfig.json
Normal file
20
server/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"noEmit": false,
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts"],
|
||||
"exclude": ["dist", "../dist", "../node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user