feat(arcade): migrate backend from NestJS to Hono/Bun

Replace @arcade/backend (NestJS) with @arcade/server (Hono/Bun).
Same two endpoints, no auth required (public game generator):
- POST /api/games/generate — AI game generation (Gemini, Claude, GPT)
- POST /api/games/submit  — Community game submission via GitHub PR
- GET  /health            — Health check

This removes the last remaining NestJS backend from the monorepo.
NestJS is now completely gone — all servers use Hono + Bun.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-31 17:02:14 +02:00
parent 7bc4db7e63
commit 6e75718cfa
26 changed files with 4662 additions and 2070 deletions

View file

@ -87,7 +87,6 @@ Always use `text` type for `user_id` columns in all database schemas.
|---------|---------|----------|
| `@manacore/shared-hono` | Hono auth middleware + helpers | All compute servers (Hono/Bun) |
| `@manacore/shared-auth` | Client auth service | Web/Mobile apps |
| `@mana-core/nestjs-integration` | Auth + Credits for NestJS | `@arcade/backend` only |
## Server Integration (Hono/Bun)
@ -113,23 +112,7 @@ app.get('/api/v1/data', (c) => {
});
```
### NestJS (arcade only)
`@arcade/backend` still uses NestJS with `@mana-core/nestjs-integration`:
```typescript
import { AuthGuard } from '@mana-core/nestjs-integration/guards';
import { CurrentUser } from '@mana-core/nestjs-integration/decorators';
@Controller('api')
@UseGuards(AuthGuard)
export class ApiController {
@Get('data')
getData(@CurrentUser() user: any) {
return this.service.findAll(user.sub);
}
}
```
> Note: Arcade's server (`@arcade/server`) does not require auth — game generation and community submission are public endpoints.
## Environment Variables

View file

@ -36,6 +36,7 @@ For comprehensive guidelines on code patterns and conventions, see the `.claude/
| **manacore** | Multi-app ecosystem platform | Mobile, Web |
| **chat** | AI chat application | Backend, Mobile, Web, Landing |
| **picture** | AI image generation | Backend, Mobile, Web, Landing |
| **memoro** | AI voice recording & memo management | Backend, Audio-Backend, Mobile, Web, Landing |
| **manadeck** | Card/deck management | Backend, Mobile, Web |
| **todo** | Task management | Backend, Web, Landing |
| **calendar** | Calendar & scheduling | Backend, Web, Landing |
@ -68,7 +69,7 @@ For comprehensive guidelines on code patterns and conventions, see the `.claude/
| Game | Description | Tech |
|------|-------------|------|
| **arcade** | AI browser games platform (22+ games) | SvelteKit, NestJS, Gemini/Claude/GPT |
| **arcade** | AI browser games platform (22+ games) | SvelteKit, Hono+Bun, Gemini/Claude/GPT |
| **voxelava** | Voxel game | SvelteKit |
| **whopixels** | Phaser.js pixel game | Phaser, JavaScript |
| **worldream** | World exploration game | SvelteKit |
@ -115,6 +116,7 @@ pnpm setup:db:auth # Setup just auth
```bash
# Start specific project (runs all apps in project)
pnpm run manacore:dev
pnpm run memoro:dev
pnpm run manadeck:dev
pnpm run picture:dev
pnpm run chat:dev
@ -161,7 +163,7 @@ manacore-monorepo/
│ ├── uload/
│ └── wisekeep/
├── games/ # Game projects
│ ├── arcade/ # AI browser games platform (SvelteKit + NestJS)
│ ├── arcade/ # AI browser games platform (SvelteKit + Hono/Bun)
│ ├── voxelava/ # Voxel game
│ ├── whopixels/ # Phaser.js pixel game
│ └── worldream/ # World exploration game

View file

@ -27,11 +27,11 @@ games/arcade/
│ │ ├── games/ # 22 HTML-Spiele
│ │ └── screenshots/ # Game-Thumbnails
│ ├── web-astro/ # Alte Astro-App (Referenz, zum Löschen)
│ └── backend/ # NestJS API (@arcade/backend)
│ └── server/ # Hono/Bun Compute Server (@arcade/server)
│ └── src/
│ ├── game-generator/ # AI-Spielgenerierung (Gemini, Claude, GPT-4)
├── game-submission/ # Community-Einreichungen (GitHub API)
│ └── health/
│ ├── routes/
│ └── games.ts # AI-Spielgenerierung + Community-Einreichungen
│ └── index.ts
└── package.json # Root (arcade)
```
@ -47,7 +47,7 @@ games/arcade/
| i18n | svelte-i18n (DE + EN) |
| UI | @manacore/shared-ui (PillNav, AuthGate, etc.) |
| Theming | @manacore/shared-theme (multi-theme) |
| Backend | NestJS (AI-Generierung, Community) |
| Server | Hono + Bun (AI-Generierung, Community) |
## Entwicklung
@ -58,8 +58,8 @@ pnpm arcade:dev
# Nur Web (SvelteKit)
pnpm dev:arcade:web
# Nur Backend (NestJS)
pnpm dev:arcade:backend
# Nur Server (Hono/Bun)
pnpm dev:arcade:server
# Web + Backend zusammen
pnpm dev:arcade:app
@ -67,7 +67,7 @@ pnpm dev:arcade:app
**Ports:**
- Web: http://localhost:5210
- Backend: http://localhost:3011
- Server: http://localhost:3011
## Local-First Daten

View file

@ -1,17 +0,0 @@
// @ts-check
import {
baseConfig,
typescriptConfig,
nestjsConfig,
prettierConfig,
} from '@manacore/eslint-config';
export default [
{
ignores: ['dist/**', 'node_modules/**'],
},
...baseConfig,
...typescriptConfig,
...nestjsConfig,
...prettierConfig,
];

View file

@ -1,8 +0,0 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

View file

@ -1,34 +0,0 @@
{
"name": "@arcade/backend",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "nest start --watch",
"build": "nest build",
"start": "nest start",
"start:prod": "node dist/main",
"lint": "eslint ."
},
"dependencies": {
"@anthropic-ai/sdk": "^0.65.0",
"openai": "^4.76.0",
"@google/genai": "^1.14.0",
"@nestjs/common": "^10.4.15",
"@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.4.15",
"@nestjs/platform-express": "^10.4.15",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^10.4.9",
"@nestjs/schematics": "^10.2.3",
"@types/express": "^5.0.0",
"@types/node": "^22.10.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.2"
}
}

View file

@ -1,17 +0,0 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { GameGeneratorModule } from './game-generator/game-generator.module';
import { GameSubmissionModule } from './game-submission/game-submission.module';
import { HealthModule } from './health/health.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
HealthModule,
GameGeneratorModule,
GameSubmissionModule,
],
})
export class AppModule {}

View file

@ -1,36 +0,0 @@
import { IsString, IsOptional, IsIn, MinLength, IsNumber } from 'class-validator';
export class GenerateGameDto {
@IsString()
@MinLength(10, { message: 'Bitte gib eine Spielbeschreibung mit mindestens 10 Zeichen ein' })
description: string;
@IsOptional()
@IsIn(['create', 'iterate'])
mode?: 'create' | 'iterate' = 'create';
@IsOptional()
@IsString()
originalPrompt?: string;
@IsOptional()
@IsString()
currentCode?: string;
@IsOptional()
@IsNumber()
iterationCount?: number = 0;
@IsOptional()
@IsString()
model?: string = 'gemini-2.0-flash';
}
export class GenerateGameResponseDto {
success: boolean;
html: string;
metadata: {
description: string;
generatedAt: string;
};
}

View file

@ -1,13 +0,0 @@
import { Controller, Post, Body } from '@nestjs/common';
import { GameGeneratorService } from './game-generator.service';
import { GenerateGameDto, GenerateGameResponseDto } from './dto/generate-game.dto';
@Controller('games')
export class GameGeneratorController {
constructor(private readonly gameGeneratorService: GameGeneratorService) {}
@Post('generate')
async generateGame(@Body() dto: GenerateGameDto): Promise<GenerateGameResponseDto> {
return this.gameGeneratorService.generateGame(dto);
}
}

View file

@ -1,9 +0,0 @@
import { Module } from '@nestjs/common';
import { GameGeneratorController } from './game-generator.controller';
import { GameGeneratorService } from './game-generator.service';
@Module({
controllers: [GameGeneratorController],
providers: [GameGeneratorService],
})
export class GameGeneratorModule {}

View file

@ -1,362 +0,0 @@
import {
Injectable,
BadRequestException,
InternalServerErrorException,
Logger,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GenerateGameDto, GenerateGameResponseDto } from './dto/generate-game.dto';
import { GoogleGenAI } from '@google/genai';
import Anthropic from '@anthropic-ai/sdk';
import { AzureOpenAI } from 'openai';
type AIProvider = 'google' | 'anthropic' | 'azure';
interface ModelConfig {
provider: AIProvider;
modelId: string;
displayName: string;
}
@Injectable()
export class GameGeneratorService {
private readonly logger = new Logger(GameGeneratorService.name);
// Model configurations
private readonly modelConfigs: Record<string, ModelConfig> = {
// Google Gemini Models
'gemini-2.0-flash': {
provider: 'google',
modelId: 'gemini-2.0-flash',
displayName: 'Gemini 2.0 Flash',
},
'gemini-2.5-flash': {
provider: 'google',
modelId: 'gemini-2.5-flash-preview-05-20',
displayName: 'Gemini 2.5 Flash',
},
'gemini-2.5-pro': {
provider: 'google',
modelId: 'gemini-2.5-pro-preview-05-06',
displayName: 'Gemini 2.5 Pro',
},
// Anthropic Claude Models
'claude-3.5-haiku': {
provider: 'anthropic',
modelId: 'claude-3-5-haiku-20241022',
displayName: 'Claude 3.5 Haiku',
},
'claude-3.5-sonnet': {
provider: 'anthropic',
modelId: 'claude-sonnet-4-20250514',
displayName: 'Claude Sonnet 4',
},
// Azure OpenAI Models
'gpt-4o': { provider: 'azure', modelId: 'gpt-4o', displayName: 'GPT-4o' },
'gpt-4o-mini': { provider: 'azure', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini' },
};
// AI Clients
private googleClient: GoogleGenAI | null = null;
private anthropicClient: Anthropic | null = null;
private azureClient: AzureOpenAI | null = null;
constructor(private readonly configService: ConfigService) {
this.initializeClients();
}
private initializeClients(): void {
// Initialize Google Gemini
const googleApiKey = this.configService.get<string>('GOOGLE_GENAI_API_KEY');
if (googleApiKey && googleApiKey !== 'your_google_genai_key_here') {
this.googleClient = new GoogleGenAI({ apiKey: googleApiKey });
this.logger.log('Google Gemini client initialized');
}
// Initialize Anthropic Claude
const anthropicApiKey = this.configService.get<string>('ANTHROPIC_API_KEY');
if (anthropicApiKey && anthropicApiKey !== 'your_anthropic_key_here') {
this.anthropicClient = new Anthropic({ apiKey: anthropicApiKey });
this.logger.log('Anthropic Claude client initialized');
}
// Initialize Azure OpenAI
const azureEndpoint = this.configService.get<string>('AZURE_OPENAI_ENDPOINT');
const azureApiKey = this.configService.get<string>('AZURE_OPENAI_API_KEY');
if (azureEndpoint && azureApiKey && azureApiKey !== 'your_azure_openai_key_here') {
this.azureClient = new AzureOpenAI({
endpoint: azureEndpoint,
apiKey: azureApiKey,
apiVersion: '2024-08-01-preview',
});
this.logger.log('Azure OpenAI client initialized');
}
}
async generateGame(dto: GenerateGameDto): Promise<GenerateGameResponseDto> {
const model = dto.model || 'gemini-2.0-flash';
const config = this.modelConfigs[model];
if (!config) {
this.logger.warn(`Unknown model: ${model}, falling back to gemini-2.0-flash`);
return this.generateGame({ ...dto, model: 'gemini-2.0-flash' });
}
// Check if the provider is available
const providerAvailable = this.isProviderAvailable(config.provider);
if (!providerAvailable) {
this.logger.error(`Provider ${config.provider} is not configured`);
throw new InternalServerErrorException(
`AI provider ${config.provider} is not configured. Please add the API key.`
);
}
// Build prompt
const prompt = this.createGamePrompt(
dto.description.trim(),
dto.mode || 'create',
dto.originalPrompt,
dto.currentCode
);
this.logger.log(
`${dto.mode === 'iterate' ? 'Iterating' : 'Generating'} game with model: ${config.displayName} (${config.provider})`
);
try {
let generatedContent: string;
switch (config.provider) {
case 'google':
generatedContent = await this.generateWithGoogle(config.modelId, prompt);
break;
case 'anthropic':
generatedContent = await this.generateWithAnthropic(config.modelId, prompt);
break;
case 'azure':
generatedContent = await this.generateWithAzure(config.modelId, prompt);
break;
default:
throw new InternalServerErrorException(`Unknown provider: ${config.provider}`);
}
// Extract HTML from response
let html = generatedContent;
const htmlMatch = generatedContent.match(/```html\n([\s\S]*?)\n```/);
if (htmlMatch) {
html = htmlMatch[1];
}
// Validate and sanitize
const safeHtml = this.validateAndSanitizeGame(html);
this.logger.log(`Game generated successfully with ${config.displayName}`);
return {
success: true,
html: safeHtml,
metadata: {
description: dto.description.trim(),
generatedAt: new Date().toISOString(),
},
};
} catch (error: any) {
if (error instanceof BadRequestException || error instanceof InternalServerErrorException) {
throw error;
}
this.logger.error(`Generation error with ${config.displayName}:`, error);
throw new InternalServerErrorException(
`Failed to generate game: ${error.message || 'Unknown error'}`
);
}
}
private isProviderAvailable(provider: AIProvider): boolean {
switch (provider) {
case 'google':
return this.googleClient !== null;
case 'anthropic':
return this.anthropicClient !== null;
case 'azure':
return this.azureClient !== null;
default:
return false;
}
}
private async generateWithGoogle(modelId: string, prompt: string): Promise<string> {
if (!this.googleClient) {
throw new InternalServerErrorException('Google Gemini client not initialized');
}
const response = await this.googleClient.models.generateContent({
model: modelId,
contents: prompt,
config: {
temperature: 0.7,
maxOutputTokens: 8192,
},
});
const content = response.text;
if (!content) {
throw new InternalServerErrorException('No content generated by Google Gemini');
}
return content;
}
private async generateWithAnthropic(modelId: string, prompt: string): Promise<string> {
if (!this.anthropicClient) {
throw new InternalServerErrorException('Anthropic Claude client not initialized');
}
const response = await this.anthropicClient.messages.create({
model: modelId,
max_tokens: 8192,
messages: [{ role: 'user', content: prompt }],
});
const content = response.content[0];
if (!content || content.type !== 'text') {
throw new InternalServerErrorException('No content generated by Anthropic Claude');
}
return content.text;
}
private async generateWithAzure(modelId: string, prompt: string): Promise<string> {
if (!this.azureClient) {
throw new InternalServerErrorException('Azure OpenAI client not initialized');
}
const deployment = this.configService.get<string>('AZURE_OPENAI_DEPLOYMENT') || modelId;
const response = await this.azureClient.chat.completions.create({
model: deployment,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 8192,
});
const content = response.choices?.[0]?.message?.content;
if (!content) {
throw new InternalServerErrorException('No content generated by Azure OpenAI');
}
return content;
}
getAvailableModels(): { id: string; name: string; provider: string; available: boolean }[] {
return Object.entries(this.modelConfigs).map(([id, config]) => ({
id,
name: config.displayName,
provider: config.provider,
available: this.isProviderAvailable(config.provider),
}));
}
private createGamePrompt(
description: string,
mode: 'create' | 'iterate',
originalPrompt?: string,
currentCode?: string
): string {
if (mode === 'iterate' && originalPrompt && currentCode) {
return `Du bist ein begabter Coder und Gamedesigner.
Der Nutzer hat ursprünglich folgendes Spiel gewünscht: "${originalPrompt}"
Jetzt möchte der Nutzer folgende Änderung: "${description}"
ERSTELLE DAS SPIEL KOMPLETT NEU mit den gewünschten Änderungen. Orientiere dich am ursprünglichen Konzept, aber implementiere die Änderungen vollständig.
WICHTIGE REGELN:
- Erstelle ein VOLLSTÄNDIGES neues HTML-Dokument
- Maximal 400 Zeilen Code insgesamt
- Nutze Canvas für die Grafik
- Das Spiel muss sofort spielbar sein
- Implementiere die gewünschten Änderungen vollständig
- PostMessage Integration: window.parent.postMessage({type: 'GAME_LOADED', gameId: 'generated'}, '*');
STRUKTUR:
<!DOCTYPE html>
<html>
<head>
<title>Spielname</title>
<style>
body { margin: 0; background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; }
canvas { border: 1px solid #333; }
</style>
</head>
<body>
<canvas id="game" width="800" height="600"></canvas>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
// Spielcode hier mit den gewünschten Änderungen
window.parent.postMessage({type: 'GAME_LOADED', gameId: 'generated'}, '*');
</script>
</body>
</html>
Schreibe nur den Code, keine weiteren Kommentare. Nutze keine externen Bibliotheken, Bilder oder Sounds.`;
}
return `Du bist ein begabter Coder und Gamedesigner. Erstelle ein HTML5-Spiel basierend auf dieser Beschreibung: ${description}
WICHTIGE REGELN:
- Maximal 400 Zeilen Code insgesamt
- Nutze Canvas für die Grafik
- Verwende einfache Formen (Rechtecke, Kreise, etc.)
- Das Spiel muss sofort spielbar sein
- Füge Steuerungshinweise im Spiel ein
- PostMessage Integration: window.parent.postMessage({type: 'GAME_LOADED', gameId: 'generated'}, '*');
STRUKTUR:
<!DOCTYPE html>
<html>
<head>
<title>Spielname</title>
<style>
body { margin: 0; background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; }
canvas { border: 1px solid #333; }
</style>
</head>
<body>
<canvas id="game" width="800" height="600"></canvas>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
// Spielcode hier
// PostMessage beim Start senden:
window.parent.postMessage({type: 'GAME_LOADED', gameId: 'generated'}, '*');
</script>
</body>
</html>
Schreibe nur den Code, keine weiteren Kommentare. Nutze keine externen Bibliotheken, Bilder oder Sounds.`;
}
private validateAndSanitizeGame(html: string): string {
if (!html || typeof html !== 'string') {
throw new BadRequestException('Invalid HTML content');
}
if (!html.includes('<!DOCTYPE html>')) {
throw new BadRequestException('Invalid game HTML structure');
}
// Security sanitization
const sanitized = html
.replace(/<script[^>]*src=[^>]*>/gi, '')
.replace(/<link[^>]*href=[^>]*>/gi, '')
.replace(/fetch\s*\(/gi, '// fetch disabled: fetch(')
.replace(/XMLHttpRequest/gi, '// XMLHttpRequest disabled')
.replace(/eval\s*\(/gi, '// eval disabled: eval(');
return sanitized;
}
}

View file

@ -1,72 +0,0 @@
import { IsString, IsArray, IsOptional, ValidateNested, IsIn } from 'class-validator';
import { Type } from 'class-transformer';
class AuthorDto {
@IsString()
name: string;
@IsOptional()
@IsString()
email?: string;
@IsOptional()
@IsString()
github?: string;
}
class FileDto {
@IsString()
name: string;
@IsString()
content: string;
}
class FilesDto {
@ValidateNested()
@Type(() => FileDto)
html: FileDto;
@ValidateNested()
@Type(() => FileDto)
screenshot: FileDto;
}
export class SubmitGameDto {
@IsString()
title: string;
@IsString()
description: string;
@IsString()
controls: string;
@IsIn(['Einfach', 'Mittel', 'Schwer'])
difficulty: string;
@IsIn(['Minimal', 'Einfach', 'Mittel', 'Komplex'])
complexity: string;
@IsArray()
@IsString({ each: true })
tags: string[];
@ValidateNested()
@Type(() => AuthorDto)
author: AuthorDto;
@ValidateNested()
@Type(() => FilesDto)
files: FilesDto;
@IsString()
submittedAt: string;
}
export class SubmitGameResponseDto {
success: boolean;
message: string;
prUrl: string;
prNumber: number;
}

View file

@ -1,13 +0,0 @@
import { Controller, Post, Body } from '@nestjs/common';
import { GameSubmissionService } from './game-submission.service';
import { SubmitGameDto, SubmitGameResponseDto } from './dto/submit-game.dto';
@Controller('games')
export class GameSubmissionController {
constructor(private readonly gameSubmissionService: GameSubmissionService) {}
@Post('submit')
async submitGame(@Body() dto: SubmitGameDto): Promise<SubmitGameResponseDto> {
return this.gameSubmissionService.submitGame(dto);
}
}

View file

@ -1,9 +0,0 @@
import { Module } from '@nestjs/common';
import { GameSubmissionController } from './game-submission.controller';
import { GameSubmissionService } from './game-submission.service';
@Module({
controllers: [GameSubmissionController],
providers: [GameSubmissionService],
})
export class GameSubmissionModule {}

View file

@ -1,237 +0,0 @@
import {
Injectable,
BadRequestException,
InternalServerErrorException,
Logger,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SubmitGameDto, SubmitGameResponseDto } from './dto/submit-game.dto';
@Injectable()
export class GameSubmissionService {
private readonly logger = new Logger(GameSubmissionService.name);
constructor(private readonly configService: ConfigService) {}
async submitGame(dto: SubmitGameDto): Promise<SubmitGameResponseDto> {
const githubToken = this.configService.get<string>('GITHUB_TOKEN');
const githubOwner = this.configService.get<string>('GITHUB_OWNER') || 'tillschneider';
const githubRepo = this.configService.get<string>('GITHUB_REPO') || 'mana-games';
if (!githubToken) {
this.logger.error('GitHub token not configured');
throw new InternalServerErrorException('Server configuration error - GitHub token missing');
}
// Generate safe file names
const gameSlug = dto.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
const timestamp = Date.now();
const branchName = `community-game-${gameSlug}-${timestamp}`;
const headers = {
Authorization: `Bearer ${githubToken}`,
Accept: 'application/vnd.github.v3+json',
'Content-Type': 'application/json',
};
try {
// 1. Get the default branch
this.logger.log(`Fetching repo: ${githubOwner}/${githubRepo}`);
const repoResponse = await fetch(
`https://api.github.com/repos/${githubOwner}/${githubRepo}`,
{ headers }
);
if (!repoResponse.ok) {
const errorBody = await repoResponse.text();
this.logger.error('GitHub API Error:', { status: repoResponse.status, body: errorBody });
throw new InternalServerErrorException(
`Failed to fetch repository info: ${repoResponse.status}`
);
}
const repoData = await repoResponse.json();
const defaultBranch = repoData.default_branch;
// 2. Get the latest commit SHA from the default branch
const refResponse = await fetch(
`https://api.github.com/repos/${githubOwner}/${githubRepo}/git/refs/heads/${defaultBranch}`,
{ headers }
);
if (!refResponse.ok) {
throw new InternalServerErrorException('Failed to fetch branch info');
}
const refData = await refResponse.json();
const baseSha = refData.object.sha;
// 3. Create a new branch
const createBranchResponse = await fetch(
`https://api.github.com/repos/${githubOwner}/${githubRepo}/git/refs`,
{
method: 'POST',
headers,
body: JSON.stringify({
ref: `refs/heads/${branchName}`,
sha: baseSha,
}),
}
);
if (!createBranchResponse.ok) {
throw new InternalServerErrorException('Failed to create branch');
}
// 4. Prepare game data
const nextId = String(Date.now());
const gameData = {
id: nextId,
title: dto.title,
description: dto.description,
slug: gameSlug,
htmlFile: `/games/${gameSlug}.html`,
thumbnail: `/screenshots/${gameSlug}.jpg`,
tags: dto.tags,
difficulty: dto.difficulty,
complexity: dto.complexity,
controls: dto.controls,
community: true,
author: dto.author.name,
submittedAt: dto.submittedAt,
};
// 5. Create files
const filesToCreate = [
{
path: `public/games/${gameSlug}.html`,
content: dto.files.html.content,
encoding: 'utf-8' as const,
},
{
path: `public/screenshots/${gameSlug}.jpg`,
content: dto.files.screenshot.content.split(',')[1], // Remove data:image/jpeg;base64,
encoding: 'base64' as const,
},
];
// Fetch existing community games
const communityGamesPath = 'src/data/community-games.json';
let communityGames: any[] = [];
try {
const existingFileResponse = await fetch(
`https://api.github.com/repos/${githubOwner}/${githubRepo}/contents/${communityGamesPath}?ref=${defaultBranch}`,
{ headers }
);
if (existingFileResponse.ok) {
const existingFile = await existingFileResponse.json();
const content = Buffer.from(existingFile.content, 'base64').toString('utf-8');
communityGames = JSON.parse(content);
}
} catch {
// File doesn't exist yet
}
communityGames.push(gameData);
filesToCreate.push({
path: communityGamesPath,
content: JSON.stringify(communityGames, null, 2),
encoding: 'utf-8',
});
// Create all files
for (const file of filesToCreate) {
const fileContent =
file.encoding === 'base64' ? file.content : Buffer.from(file.content).toString('base64');
const createFileResponse = await fetch(
`https://api.github.com/repos/${githubOwner}/${githubRepo}/contents/${file.path}`,
{
method: 'PUT',
headers,
body: JSON.stringify({
message: `Add community game: ${dto.title}`,
content: fileContent,
branch: branchName,
}),
}
);
if (!createFileResponse.ok) {
const error = await createFileResponse.text();
this.logger.error(`Failed to create file ${file.path}:`, error);
throw new InternalServerErrorException(`Failed to create file ${file.path}`);
}
}
// 6. Create pull request
const prBody = `## Neues Community-Spiel: ${dto.title}
### Spiel-Details
- **Autor:** ${dto.author.name}${dto.author.github ? ` (@${dto.author.github})` : ''}
- **Beschreibung:** ${dto.description}
- **Schwierigkeit:** ${dto.difficulty}
- **Komplexität:** ${dto.complexity}
- **Steuerung:** ${dto.controls}
- **Tags:** ${dto.tags.join(', ')}
### Dateien
- HTML: \`public/games/${gameSlug}.html\`
- Screenshot: \`public/screenshots/${gameSlug}.jpg\`
### Checkliste für Review
- [ ] Spiel funktioniert einwandfrei
- [ ] Keine externen Abhängigkeiten oder Sicherheitsprobleme
- [ ] Familienfreundlicher Inhalt
- [ ] Screenshot zeigt das Spiel korrekt
- [ ] postMessage Integration vorhanden (optional)
---
*Eingereicht am: ${new Date(dto.submittedAt).toLocaleString('de-DE')}*
${dto.author.email ? `*Kontakt: ${dto.author.email}*` : ''}`;
const prResponse = await fetch(
`https://api.github.com/repos/${githubOwner}/${githubRepo}/pulls`,
{
method: 'POST',
headers,
body: JSON.stringify({
title: `Community: ${dto.title}`,
body: prBody,
head: branchName,
base: defaultBranch,
}),
}
);
if (!prResponse.ok) {
const error = await prResponse.text();
this.logger.error('Failed to create PR:', error);
throw new InternalServerErrorException('Failed to create pull request');
}
const prData = await prResponse.json();
return {
success: true,
message: 'Game submitted successfully',
prUrl: prData.html_url,
prNumber: prData.number,
};
} catch (error: any) {
this.logger.error('Submission error:', error);
if (error instanceof BadRequestException || error instanceof InternalServerErrorException) {
throw error;
}
throw new InternalServerErrorException(
'Failed to submit game: ' + (error.message || 'Unknown error')
);
}
}
}

View file

@ -1,13 +0,0 @@
import { Controller, Get } from '@nestjs/common';
@Controller('health')
export class HealthController {
@Get()
check() {
return {
status: 'ok',
timestamp: new Date().toISOString(),
service: 'mana-games-backend',
};
}
}

View file

@ -1,7 +0,0 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
@Module({
controllers: [HealthController],
})
export class HealthModule {}

View file

@ -1,37 +0,0 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// CORS configuration
app.enableCors({
origin: [
'http://localhost:5210', // SvelteKit dev
'http://localhost:4321', // Legacy Astro dev
'http://localhost:3000', // Alternative dev
],
methods: ['GET', 'POST', 'OPTIONS'],
credentials: false,
});
app.setGlobalPrefix('api');
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
})
);
const port = process.env.PORT || 3010;
// Increase timeout for long-running AI requests (2 minutes)
const server = await app.listen(port);
server.setTimeout(120000);
console.log(`Arcade backend running on http://localhost:${port}`);
}
bootstrap();

View file

@ -1,22 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true
}
}

View file

@ -0,0 +1,20 @@
{
"name": "@arcade/server",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "bun run --watch src/index.ts",
"build": "bun build src/index.ts --outdir dist --target bun",
"start": "bun run src/index.ts"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.65.0",
"@google/genai": "^1.14.0",
"hono": "^4.7.5",
"openai": "^4.76.0"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.7.2"
}
}

View file

@ -0,0 +1,27 @@
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { gamesRoutes } from './routes/games';
const PORT = parseInt(process.env.PORT || '3011', 10);
const CORS_ORIGINS = (process.env.CORS_ORIGINS || 'http://localhost:5173').split(',');
const app = new Hono();
app.onError((err, c) => {
console.error('Unhandled error:', err);
return c.json({ error: 'Internal server error' }, 500);
});
app.notFound((c) => c.json({ error: 'Not found' }, 404));
app.use('*', cors({ origin: CORS_ORIGINS, credentials: false }));
app.get('/health', (c) =>
c.json({ status: 'ok', timestamp: new Date().toISOString(), service: 'arcade-server' })
);
app.route('/api/games', gamesRoutes);
console.log(`Arcade server running on http://localhost:${PORT}`);
export default { port: PORT, fetch: app.fetch };

View file

@ -0,0 +1,432 @@
import { Hono } from 'hono';
import { GoogleGenAI } from '@google/genai';
import Anthropic from '@anthropic-ai/sdk';
import { AzureOpenAI } from 'openai';
type AIProvider = 'google' | 'anthropic' | 'azure';
interface ModelConfig {
provider: AIProvider;
modelId: string;
displayName: string;
}
const MODEL_CONFIGS: Record<string, ModelConfig> = {
'gemini-2.0-flash': {
provider: 'google',
modelId: 'gemini-2.0-flash',
displayName: 'Gemini 2.0 Flash',
},
'gemini-2.5-flash': {
provider: 'google',
modelId: 'gemini-2.5-flash-preview-05-20',
displayName: 'Gemini 2.5 Flash',
},
'gemini-2.5-pro': {
provider: 'google',
modelId: 'gemini-2.5-pro-preview-05-06',
displayName: 'Gemini 2.5 Pro',
},
'claude-3.5-haiku': {
provider: 'anthropic',
modelId: 'claude-3-5-haiku-20241022',
displayName: 'Claude 3.5 Haiku',
},
'claude-3.5-sonnet': {
provider: 'anthropic',
modelId: 'claude-sonnet-4-20250514',
displayName: 'Claude Sonnet 4',
},
'gpt-4o': { provider: 'azure', modelId: 'gpt-4o', displayName: 'GPT-4o' },
'gpt-4o-mini': { provider: 'azure', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini' },
};
function initClients() {
const googleKey = process.env.GOOGLE_GENAI_API_KEY;
const anthropicKey = process.env.ANTHROPIC_API_KEY;
const azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
const azureKey = process.env.AZURE_OPENAI_API_KEY;
return {
google:
googleKey && !googleKey.includes('your_') ? new GoogleGenAI({ apiKey: googleKey }) : null,
anthropic:
anthropicKey && !anthropicKey.includes('your_')
? new Anthropic({ apiKey: anthropicKey })
: null,
azure:
azureEndpoint && azureKey && !azureKey.includes('your_')
? new AzureOpenAI({
endpoint: azureEndpoint,
apiKey: azureKey,
apiVersion: '2024-08-01-preview',
})
: null,
};
}
const clients = initClients();
function createGamePrompt(
description: string,
mode: 'create' | 'iterate',
originalPrompt?: string,
currentCode?: string
): string {
if (mode === 'iterate' && originalPrompt && currentCode) {
return `Du bist ein begabter Coder und Gamedesigner.
Der Nutzer hat ursprünglich folgendes Spiel gewünscht: "${originalPrompt}"
Jetzt möchte der Nutzer folgende Änderung: "${description}"
ERSTELLE DAS SPIEL KOMPLETT NEU mit den gewünschten Änderungen. Orientiere dich am ursprünglichen Konzept, aber implementiere die Änderungen vollständig.
WICHTIGE REGELN:
- Erstelle ein VOLLSTÄNDIGES neues HTML-Dokument
- Maximal 400 Zeilen Code insgesamt
- Nutze Canvas für die Grafik
- Das Spiel muss sofort spielbar sein
- Implementiere die gewünschten Änderungen vollständig
- PostMessage Integration: window.parent.postMessage({type: 'GAME_LOADED', gameId: 'generated'}, '*');
STRUKTUR:
<!DOCTYPE html>
<html>
<head>
<title>Spielname</title>
<style>
body { margin: 0; background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; }
canvas { border: 1px solid #333; }
</style>
</head>
<body>
<canvas id="game" width="800" height="600"></canvas>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
// Spielcode hier mit den gewünschten Änderungen
window.parent.postMessage({type: 'GAME_LOADED', gameId: 'generated'}, '*');
</script>
</body>
</html>
Schreibe nur den Code, keine weiteren Kommentare. Nutze keine externen Bibliotheken, Bilder oder Sounds.`;
}
return `Du bist ein begabter Coder und Gamedesigner. Erstelle ein HTML5-Spiel basierend auf dieser Beschreibung: ${description}
WICHTIGE REGELN:
- Maximal 400 Zeilen Code insgesamt
- Nutze Canvas für die Grafik
- Verwende einfache Formen (Rechtecke, Kreise, etc.)
- Das Spiel muss sofort spielbar sein
- Füge Steuerungshinweise im Spiel ein
- PostMessage Integration: window.parent.postMessage({type: 'GAME_LOADED', gameId: 'generated'}, '*');
STRUKTUR:
<!DOCTYPE html>
<html>
<head>
<title>Spielname</title>
<style>
body { margin: 0; background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; }
canvas { border: 1px solid #333; }
</style>
</head>
<body>
<canvas id="game" width="800" height="600"></canvas>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
// Spielcode hier
// PostMessage beim Start senden:
window.parent.postMessage({type: 'GAME_LOADED', gameId: 'generated'}, '*');
</script>
</body>
</html>
Schreibe nur den Code, keine weiteren Kommentare. Nutze keine externen Bibliotheken, Bilder oder Sounds.`;
}
function validateAndSanitizeGame(html: string): string {
if (!html || typeof html !== 'string') {
throw new Error('Invalid HTML content');
}
if (!html.includes('<!DOCTYPE html>')) {
throw new Error('Invalid game HTML structure');
}
return html
.replace(/<script[^>]*src=[^>]*>/gi, '')
.replace(/<link[^>]*href=[^>]*>/gi, '')
.replace(/fetch\s*\(/gi, '// fetch disabled: fetch(')
.replace(/XMLHttpRequest/gi, '// XMLHttpRequest disabled')
.replace(/eval\s*\(/gi, '// eval disabled: eval(');
}
async function generateWithProvider(config: ModelConfig, prompt: string): Promise<string> {
if (config.provider === 'google') {
if (!clients.google) throw new Error('Google Gemini not configured');
const response = await clients.google.models.generateContent({
model: config.modelId,
contents: prompt,
config: { temperature: 0.7, maxOutputTokens: 8192 },
});
const content = response.text;
if (!content) throw new Error('No content from Google Gemini');
return content;
}
if (config.provider === 'anthropic') {
if (!clients.anthropic) throw new Error('Anthropic not configured');
const response = await clients.anthropic.messages.create({
model: config.modelId,
max_tokens: 8192,
messages: [{ role: 'user', content: prompt }],
});
const content = response.content[0];
if (!content || content.type !== 'text') throw new Error('No content from Anthropic');
return content.text;
}
if (config.provider === 'azure') {
if (!clients.azure) throw new Error('Azure OpenAI not configured');
const deployment = process.env.AZURE_OPENAI_DEPLOYMENT || config.modelId;
const response = await clients.azure.chat.completions.create({
model: deployment,
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
max_tokens: 8192,
});
const content = response.choices?.[0]?.message?.content;
if (!content) throw new Error('No content from Azure OpenAI');
return content;
}
throw new Error(`Unknown provider: ${config.provider}`);
}
export const gamesRoutes = new Hono();
gamesRoutes.post('/generate', async (c) => {
const body = await c.req.json().catch(() => null);
if (!body) return c.json({ error: 'Invalid JSON body' }, 400);
const {
description,
mode = 'create',
originalPrompt,
currentCode,
model = 'gemini-2.0-flash',
} = body;
if (!description || typeof description !== 'string' || description.trim().length < 10) {
return c.json({ error: 'Bitte gib eine Spielbeschreibung mit mindestens 10 Zeichen ein' }, 400);
}
const config = MODEL_CONFIGS[model] ?? MODEL_CONFIGS['gemini-2.0-flash'];
const isAvailable =
(config.provider === 'google' && clients.google !== null) ||
(config.provider === 'anthropic' && clients.anthropic !== null) ||
(config.provider === 'azure' && clients.azure !== null);
if (!isAvailable) {
return c.json({ error: `AI provider ${config.provider} is not configured` }, 500);
}
const prompt = createGamePrompt(description.trim(), mode, originalPrompt, currentCode);
try {
let raw = await generateWithProvider(config, prompt);
const htmlMatch = raw.match(/```html\n([\s\S]*?)\n```/);
if (htmlMatch) raw = htmlMatch[1];
const html = validateAndSanitizeGame(raw);
return c.json({
success: true,
html,
metadata: { description: description.trim(), generatedAt: new Date().toISOString() },
});
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Unknown error';
return c.json({ error: `Failed to generate game: ${message}` }, 500);
}
});
gamesRoutes.post('/submit', async (c) => {
const body = await c.req.json().catch(() => null);
if (!body) return c.json({ error: 'Invalid JSON body' }, 400);
const { title, description, controls, difficulty, complexity, tags, author, files, submittedAt } =
body;
if (!title || !description || !files?.html?.content || !files?.screenshot?.content) {
return c.json({ error: 'Missing required fields' }, 400);
}
const githubToken = process.env.GITHUB_TOKEN;
const githubOwner = process.env.GITHUB_OWNER || 'tillschneider';
const githubRepo = process.env.GITHUB_REPO || 'mana-games';
if (!githubToken || githubToken.includes('your_')) {
return c.json({ error: 'Server configuration error - GitHub token missing' }, 500);
}
const gameSlug = title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
const timestamp = Date.now();
const branchName = `community-game-${gameSlug}-${timestamp}`;
const headers = {
Authorization: `Bearer ${githubToken}`,
Accept: 'application/vnd.github.v3+json',
'Content-Type': 'application/json',
};
try {
const repoResponse = await fetch(`https://api.github.com/repos/${githubOwner}/${githubRepo}`, {
headers,
});
if (!repoResponse.ok) {
return c.json({ error: `Failed to fetch repository info: ${repoResponse.status}` }, 500);
}
const repoData = (await repoResponse.json()) as { default_branch: string };
const defaultBranch = repoData.default_branch;
const refResponse = await fetch(
`https://api.github.com/repos/${githubOwner}/${githubRepo}/git/refs/heads/${defaultBranch}`,
{ headers }
);
if (!refResponse.ok) return c.json({ error: 'Failed to fetch branch info' }, 500);
const refData = (await refResponse.json()) as { object: { sha: string } };
const baseSha = refData.object.sha;
const createBranchResponse = await fetch(
`https://api.github.com/repos/${githubOwner}/${githubRepo}/git/refs`,
{
method: 'POST',
headers,
body: JSON.stringify({ ref: `refs/heads/${branchName}`, sha: baseSha }),
}
);
if (!createBranchResponse.ok) return c.json({ error: 'Failed to create branch' }, 500);
const gameData = {
id: String(timestamp),
title,
description,
slug: gameSlug,
htmlFile: `/games/${gameSlug}.html`,
thumbnail: `/screenshots/${gameSlug}.jpg`,
tags,
difficulty,
complexity,
controls,
community: true,
author: author.name,
submittedAt,
};
const communityGamesPath = 'src/data/community-games.json';
let communityGames: unknown[] = [];
const existingFileResponse = await fetch(
`https://api.github.com/repos/${githubOwner}/${githubRepo}/contents/${communityGamesPath}?ref=${defaultBranch}`,
{ headers }
);
if (existingFileResponse.ok) {
const existingFile = (await existingFileResponse.json()) as { content: string };
const content = Buffer.from(existingFile.content, 'base64').toString('utf-8');
communityGames = JSON.parse(content);
}
communityGames.push(gameData);
const filesToCreate = [
{
path: `public/games/${gameSlug}.html`,
content: Buffer.from(files.html.content).toString('base64'),
},
{
path: `public/screenshots/${gameSlug}.jpg`,
content: files.screenshot.content.split(',')[1],
},
{
path: communityGamesPath,
content: Buffer.from(JSON.stringify(communityGames, null, 2)).toString('base64'),
},
];
for (const file of filesToCreate) {
const res = await fetch(
`https://api.github.com/repos/${githubOwner}/${githubRepo}/contents/${file.path}`,
{
method: 'PUT',
headers,
body: JSON.stringify({
message: `Add community game: ${title}`,
content: file.content,
branch: branchName,
}),
}
);
if (!res.ok) return c.json({ error: `Failed to create file ${file.path}` }, 500);
}
const prBody = `## Neues Community-Spiel: ${title}
### Spiel-Details
- **Autor:** ${author.name}${author.github ? ` (@${author.github})` : ''}
- **Beschreibung:** ${description}
- **Schwierigkeit:** ${difficulty}
- **Komplexität:** ${complexity}
- **Steuerung:** ${controls}
- **Tags:** ${(tags as string[]).join(', ')}
### Dateien
- HTML: \`public/games/${gameSlug}.html\`
- Screenshot: \`public/screenshots/${gameSlug}.jpg\`
### Checkliste für Review
- [ ] Spiel funktioniert einwandfrei
- [ ] Keine externen Abhängigkeiten oder Sicherheitsprobleme
- [ ] Familienfreundlicher Inhalt
- [ ] Screenshot zeigt das Spiel korrekt
---
*Eingereicht am: ${new Date(submittedAt).toLocaleString('de-DE')}*
${author.email ? `*Kontakt: ${author.email}*` : ''}`;
const prResponse = await fetch(
`https://api.github.com/repos/${githubOwner}/${githubRepo}/pulls`,
{
method: 'POST',
headers,
body: JSON.stringify({
title: `Community: ${title}`,
body: prBody,
head: branchName,
base: defaultBranch,
}),
}
);
if (!prResponse.ok) return c.json({ error: 'Failed to create pull request' }, 500);
const prData = (await prResponse.json()) as { html_url: string; number: number };
return c.json({
success: true,
message: 'Game submitted successfully',
prUrl: prData.html_url,
prNumber: prData.number,
});
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Unknown error';
return c.json({ error: `Failed to submit game: ${message}` }, 500);
}
});

View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowImportingTsExtensions": true,
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true
}
}

View file

@ -52,6 +52,14 @@
"dev:sync": "cd services/mana-sync && JWKS_URL=http://localhost:3001/api/auth/jwks DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/mana_sync ./server",
"dev:sync:build": "cd services/mana-sync && go build -o server ./cmd/server",
"dev:chat:full": "./scripts/setup-databases.sh chat && ./scripts/setup-databases.sh auth && concurrently -n auth,sync,server,web -c blue,magenta,yellow,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:chat:server\" \"pnpm dev:chat:web\"",
"memoro:dev": "turbo run dev --filter=memoro...",
"dev:memoro:web": "pnpm --filter @memoro/web dev",
"dev:memoro:mobile": "pnpm --filter @memoro/mobile start",
"dev:memoro:landing": "pnpm --filter @memoro/landing dev",
"dev:memoro:backend": "pnpm --filter @memoro/backend start:dev",
"dev:memoro:audio-backend": "pnpm --filter @memoro/audio-backend start:dev",
"dev:memoro:app": "concurrently -n backend,web -c yellow,cyan \"pnpm dev:memoro:backend\" \"pnpm dev:memoro:web\"",
"dev:memoro:full": "concurrently -n auth,sync,backend,web -c blue,magenta,yellow,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:memoro:backend\" \"pnpm dev:memoro:web\"",
"zitare:dev": "turbo run dev --filter=zitare...",
"dev:zitare:mobile": "pnpm --filter @zitare/mobile dev",
"dev:zitare:web": "pnpm --filter @zitare/web dev",
@ -136,8 +144,8 @@
"dev:voxel-lava:web": "pnpm --filter @voxel-lava/web dev",
"arcade:dev": "turbo run dev --filter=arcade...",
"dev:arcade:web": "pnpm --filter @arcade/web dev",
"dev:arcade:backend": "pnpm --filter @arcade/backend dev",
"dev:arcade:app": "turbo run dev --filter=@arcade/web --filter=@arcade/backend",
"dev:arcade:server": "pnpm --filter @arcade/server dev",
"dev:arcade:app": "turbo run dev --filter=@arcade/web --filter=@arcade/server",
"figgos:dev": "turbo run dev --filter=figgos...",
"dev:figgos:mobile": "pnpm --filter @figgos/mobile dev",
"dev:figgos:web": "pnpm --filter @figgos/web dev",

5275
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -282,9 +282,9 @@ const APP_CONFIGS = [
},
},
// Arcade Backend (NestJS)
// Arcade Server (Hono/Bun)
{
path: 'games/arcade/apps/backend/.env',
path: 'games/arcade/apps/server/.env',
vars: {
NODE_ENV: () => 'development',
PORT: (env) => env.MANA_GAMES_BACKEND_PORT || '3011',