refactor: bare structure for new backend architecture and runtime; no behavior changes yet

This commit is contained in:
Haileyesus
2026-03-12 14:17:12 +03:00
parent b54cdf8168
commit e67738c9fc
40 changed files with 10316 additions and 9 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

19
server/src/app.ts Normal file
View 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
View File

@@ -0,0 +1,8 @@
import { createServerApplication } from './app.js';
async function startServerApplication(): Promise<void> {
const application = createServerApplication();
await application.start();
}
await startServerApplication();

View 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'),
};
}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View 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',
},
};

View 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,
};
}

View 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);
};
}

View 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);
}

View 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);
}

View 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();
}

View 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>;
};

View 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';
};

View 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;
}
}

View 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
View 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"]
}