scaffold: pnpm-monorepo (apps/api+web, packages/shared) + docker-compose + codegen-orchestrator
This commit is contained in:
parent
170deb7b4d
commit
940e492359
3
GENERATION_LOG.md
Normal file
3
GENERATION_LOG.md
Normal file
@ -0,0 +1,3 @@
|
||||
# EmberClone — Generation Log
|
||||
|
||||
Schritt-für-Schritt-Historie aller Gemma-Code-Generierungen.
|
||||
10
apps/api/drizzle.config.ts
Normal file
10
apps/api/drizzle.config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { Config } from "drizzle-kit"
|
||||
|
||||
export default {
|
||||
schema: "./src/db/schema.ts",
|
||||
out: "./drizzle",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL || "postgresql://emberclone:emberclone@localhost:5433/emberclone",
|
||||
},
|
||||
} satisfies Config
|
||||
31
apps/api/package.json
Normal file
31
apps/api/package.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@emberclone/api",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/index.js",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "tsx src/db/migrate.ts",
|
||||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^9.3.1",
|
||||
"@fastify/cors": "^9.0.1",
|
||||
"@fastify/jwt": "^8.0.1",
|
||||
"argon2": "^0.40.3",
|
||||
"drizzle-orm": "^0.36.0",
|
||||
"fastify": "^4.28.1",
|
||||
"pg": "^8.13.1",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/pg": "^8.11.10",
|
||||
"drizzle-kit": "^0.28.0",
|
||||
"tsx": "^4.19.1",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
16
apps/api/tsconfig.json
Normal file
16
apps/api/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"allowImportingTsExtensions": false,
|
||||
"verbatimModuleSyntax": false
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
12
apps/web/index.html
Normal file
12
apps/web/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>EmberClone</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
27
apps/web/package.json
Normal file
27
apps/web/package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@emberclone/web",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 5174",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview --port 5174"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.59.0",
|
||||
"@tanstack/react-router": "^1.62.7",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.8"
|
||||
}
|
||||
}
|
||||
17
apps/web/tsconfig.json
Normal file
17
apps/web/tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
15
apps/web/vite.config.ts
Normal file
15
apps/web/vite.config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5174,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:4001",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
22
infra/docker-compose.yml
Normal file
22
infra/docker-compose.yml
Normal file
@ -0,0 +1,22 @@
|
||||
name: emberclone
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: emberclone-postgres
|
||||
ports:
|
||||
- "5433:5432"
|
||||
environment:
|
||||
POSTGRES_USER: emberclone
|
||||
POSTGRES_PASSWORD: emberclone
|
||||
POSTGRES_DB: emberclone
|
||||
volumes:
|
||||
- emberclone_pg_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U emberclone"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
emberclone_pg_data:
|
||||
11
packages/shared/package.json
Normal file
11
packages/shared/package.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@emberclone/shared",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"dependencies": {
|
||||
"zod": "^3.23.8"
|
||||
}
|
||||
}
|
||||
446
scripts/codegen.py
Normal file
446
scripts/codegen.py
Normal file
@ -0,0 +1,446 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
EmberClone codegen orchestrator.
|
||||
|
||||
Iterates through a planned list of files-to-generate, prompts Gemma for each,
|
||||
verifies output (basic syntax for TS/TSX), commits to git, logs to
|
||||
GENERATION_LOG.md.
|
||||
|
||||
Usage:
|
||||
python3 scripts/codegen.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
LOG = ROOT / "GENERATION_LOG.md"
|
||||
VLLM_URL = "http://127.0.0.1:8000/v1/chat/completions"
|
||||
MODEL = "gemma-4-31b"
|
||||
MAX_RETRIES = 3
|
||||
|
||||
# --- Blueprint context (cached, loaded once) ---
|
||||
BLUEPRINT_PATH = (
|
||||
Path.home()
|
||||
/ "AI-Memory"
|
||||
/ "claude-cache"
|
||||
/ "saas-blueprints"
|
||||
/ "embertime"
|
||||
/ "blueprint"
|
||||
/ "BLUEPRINT.md"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileSpec:
|
||||
path: str # relative to ROOT
|
||||
purpose: str
|
||||
deps: list[str] = field(default_factory=list) # other generated files to inline as context
|
||||
extra_context: str = ""
|
||||
extension: str = "" # ts/tsx/css/json — for syntax hints
|
||||
# filled at runtime
|
||||
attempts: int = 0
|
||||
ok: bool = False
|
||||
final_error: str | None = None
|
||||
|
||||
|
||||
def log(msg: str, level: str = "INFO") -> None:
|
||||
ts = datetime.datetime.now().strftime("%H:%M:%S")
|
||||
LOG.parent.mkdir(exist_ok=True, parents=True)
|
||||
with LOG.open("a") as f:
|
||||
f.write(f"- `{ts}` **{level}** {msg}\n")
|
||||
print(f"[{ts} {level}] {msg}", flush=True)
|
||||
|
||||
|
||||
def log_section(title: str) -> None:
|
||||
ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
with LOG.open("a") as f:
|
||||
f.write(f"\n## {title} ({ts})\n\n")
|
||||
print(f"\n=== {title} ===", flush=True)
|
||||
|
||||
|
||||
def git(*args: str) -> tuple[int, str]:
|
||||
result = subprocess.run(
|
||||
["git", "-c", "user.email=dennis.paradzinski@it.financeflow.de",
|
||||
"-c", "user.name=Dennis (via Claude+Gemma)", *args],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.returncode, (result.stdout + result.stderr).strip()
|
||||
|
||||
|
||||
async def gemma_call(prompt: str, max_tokens: int = 6000, temperature: float = 0.2) -> str | None:
|
||||
"""One call to Gemma. Returns text or None on failure."""
|
||||
async with httpx.AsyncClient(timeout=600) as client:
|
||||
try:
|
||||
r = await client.post(
|
||||
VLLM_URL,
|
||||
json={
|
||||
"model": MODEL,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
},
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()["choices"][0]["message"]["content"]
|
||||
except Exception as e:
|
||||
log(f"Gemma-Call-Fehler: {type(e).__name__}: {e}", level="ERROR")
|
||||
return None
|
||||
|
||||
|
||||
def strip_codefence(text: str) -> str:
|
||||
"""Remove markdown code-fences if present."""
|
||||
t = text.strip()
|
||||
if t.startswith("```"):
|
||||
lines = t.split("\n")
|
||||
if lines[0].startswith("```"):
|
||||
lines = lines[1:]
|
||||
if lines and lines[-1].startswith("```"):
|
||||
lines = lines[:-1]
|
||||
t = "\n".join(lines)
|
||||
return t.strip()
|
||||
|
||||
|
||||
def basic_syntax_check(path: Path, ext: str) -> tuple[bool, str]:
|
||||
"""Cheap structural validation per file-type. Returns (ok, error_msg)."""
|
||||
try:
|
||||
content = path.read_text()
|
||||
except Exception as e:
|
||||
return False, f"can't read: {e}"
|
||||
|
||||
if not content.strip():
|
||||
return False, "empty file"
|
||||
if len(content) < 30:
|
||||
return False, f"too short ({len(content)} chars)"
|
||||
|
||||
if ext == "json":
|
||||
try:
|
||||
json.loads(content)
|
||||
return True, ""
|
||||
except Exception as e:
|
||||
return False, f"invalid JSON: {e}"
|
||||
|
||||
if ext in {"ts", "tsx"}:
|
||||
if content.count("{") != content.count("}"):
|
||||
return False, "unbalanced braces"
|
||||
if content.count("(") != content.count(")"):
|
||||
return False, "unbalanced parens"
|
||||
|
||||
return True, ""
|
||||
|
||||
|
||||
def build_prompt(spec: FileSpec, blueprint_excerpt: str) -> str:
|
||||
deps_text = ""
|
||||
for dep in spec.deps:
|
||||
dep_path = ROOT / dep
|
||||
if dep_path.exists():
|
||||
content = dep_path.read_text()
|
||||
deps_text += f"\n\n### `{dep}` (bereits existent, referenzieren wo passend):\n```{Path(dep).suffix.lstrip('.')}\n{content[:3000]}\n```"
|
||||
|
||||
return f"""Du generierst eine einzelne Datei für EmberClone, eine Zeiterfassungs-Anwendung.
|
||||
|
||||
**EmberClone-Architektur** (pnpm-Monorepo):
|
||||
- `apps/api` — Fastify + Drizzle + PostgreSQL, Port :4001
|
||||
- `apps/web` — React + Vite + TanStack Query/Router + Tailwind, Port :5174, Vite-Proxy `/api` → `:4001`
|
||||
- `packages/shared` — Zod-Schemas, geteilt zwischen api+web
|
||||
- Auth: JWT via @fastify/jwt
|
||||
- Sprache: TypeScript strict
|
||||
- Code-Style: 2-space-indent, double-quotes, semicolons aus (siehe biome)
|
||||
|
||||
**Blueprint-Context** (aus der Analyse von Embertime):
|
||||
```markdown
|
||||
{blueprint_excerpt[:2500]}
|
||||
```
|
||||
{deps_text}
|
||||
|
||||
---
|
||||
|
||||
**Aufgabe:** Generiere die Datei `{spec.path}`.
|
||||
|
||||
**Zweck:** {spec.purpose}
|
||||
|
||||
{spec.extra_context}
|
||||
|
||||
**Output-Format:** ANTWORTE NUR MIT DEM DATEI-INHALT. Keine Erklärungen, keine Code-Fences (```), keine Kommentare wie "// Hier ist die Datei". Direkt der Code als wäre es schon die Datei.
|
||||
"""
|
||||
|
||||
|
||||
async def generate_file(spec: FileSpec, blueprint: str) -> bool:
|
||||
log_section(f"Generiere {spec.path}")
|
||||
path = ROOT / spec.path
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
last_error = ""
|
||||
for attempt in range(MAX_RETRIES):
|
||||
spec.attempts = attempt + 1
|
||||
log(f"Attempt {attempt + 1}/{MAX_RETRIES} für {spec.path}")
|
||||
retry_hint = ""
|
||||
if attempt > 0 and last_error:
|
||||
retry_hint = f"\n\n**Vorheriger Versuch fehlgeschlagen mit:** {last_error}. Bitte korrigieren."
|
||||
|
||||
prompt = build_prompt(spec, blueprint) + retry_hint
|
||||
t0 = time.time()
|
||||
response = await gemma_call(prompt)
|
||||
dt = time.time() - t0
|
||||
if not response:
|
||||
last_error = "no response from Gemma"
|
||||
log(f" no response ({dt:.1f}s)", level="WARN")
|
||||
continue
|
||||
|
||||
content = strip_codefence(response)
|
||||
path.write_text(content)
|
||||
log(f" wrote {len(content)} chars in {dt:.1f}s")
|
||||
|
||||
ok, err = basic_syntax_check(path, spec.extension)
|
||||
if ok:
|
||||
log(f" syntax check ok", level="INFO")
|
||||
spec.ok = True
|
||||
return True
|
||||
else:
|
||||
last_error = err
|
||||
log(f" syntax check failed: {err}", level="WARN")
|
||||
|
||||
spec.final_error = last_error
|
||||
log(f" GAVE UP after {MAX_RETRIES} attempts: {last_error}", level="ERROR")
|
||||
return False
|
||||
|
||||
|
||||
# ---- File specs in dependency order ----
|
||||
|
||||
SPECS: list[FileSpec] = [
|
||||
FileSpec(
|
||||
path="packages/shared/src/schemas.ts",
|
||||
purpose=(
|
||||
"Zod-Schemas für die Entitäten User, TimeEntry, Project, Customer. "
|
||||
"Pro Entität: ein Insert-Schema + Select-Schema (mit ID, timestamps). "
|
||||
"Plus Login-Request-Schema (email + password). "
|
||||
"Exportiere alle Schemas + die zugehörigen TypeScript-Types via z.infer<typeof X>."
|
||||
),
|
||||
extension="ts",
|
||||
extra_context=(
|
||||
"Felder: User(id,email,name,role:'admin'|'user',passwordHash,createdAt). "
|
||||
"TimeEntry(id,userId,projectId?,description,startTime,endTime?,createdAt). "
|
||||
"Project(id,name,customerId,active,createdAt). "
|
||||
"Customer(id,name,active,createdAt)."
|
||||
),
|
||||
),
|
||||
FileSpec(
|
||||
path="packages/shared/src/index.ts",
|
||||
purpose="Re-export aller Schemas und Types aus schemas.ts via `export * from './schemas'`.",
|
||||
deps=["packages/shared/src/schemas.ts"],
|
||||
extension="ts",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/api/src/db/schema.ts",
|
||||
purpose=(
|
||||
"Drizzle-ORM Postgres-Schema für die Tabellen users, time_entries, projects, customers. "
|
||||
"Spalten matchen die Felder aus packages/shared/src/schemas.ts. "
|
||||
"Verwende pgTable, uuid().primaryKey().defaultRandom(), timestamp().notNull().defaultNow(), text().notNull(), boolean(). "
|
||||
"Foreign Keys mit references() und onDelete: 'cascade' wo passend."
|
||||
),
|
||||
deps=["packages/shared/src/schemas.ts"],
|
||||
extension="ts",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/api/src/db/index.ts",
|
||||
purpose=(
|
||||
"Drizzle-DB-Connection aus DATABASE_URL env var (default postgresql://emberclone:emberclone@localhost:5433/emberclone). "
|
||||
"Exportiert: `db` (drizzle-Instanz), `pool` (pg.Pool), und das schema."
|
||||
),
|
||||
deps=["apps/api/src/db/schema.ts"],
|
||||
extension="ts",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/api/src/db/migrate.ts",
|
||||
purpose=(
|
||||
"Migration-Runner-Script. Importiert die Drizzle-Instanz und ruft `await migrate(db, { migrationsFolder: './drizzle' })`. "
|
||||
"Anschließend: prüft ob ein Admin-User existiert, wenn nicht, legt einen Default-Admin an: email='admin@emberclone.local', password='emberclone2026', "
|
||||
"name='Admin', role='admin'. Passwort mit argon2 hashen. Logged Erfolg/Fehler und exit(0)."
|
||||
),
|
||||
deps=["apps/api/src/db/index.ts", "apps/api/src/db/schema.ts"],
|
||||
extension="ts",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/api/src/routes/auth.ts",
|
||||
purpose=(
|
||||
"Fastify-Plugin für Auth-Routes: POST /api/auth/login (email+password → JWT in cookie), "
|
||||
"GET /api/auth/me (returns current user from JWT), POST /api/auth/logout (clear cookie). "
|
||||
"Verwende @fastify/jwt. Passwort-Check mit argon2.verify. "
|
||||
"Zod-Validierung des Request-Body via @emberclone/shared LoginRequestSchema."
|
||||
),
|
||||
deps=["apps/api/src/db/index.ts", "apps/api/src/db/schema.ts"],
|
||||
extension="ts",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/api/src/routes/time-entries.ts",
|
||||
purpose=(
|
||||
"Fastify-Plugin für CRUD /api/time-entries. "
|
||||
"GET / (list, filter by date range via query ?from=...&to=...), "
|
||||
"GET /:id (single), POST / (create), PATCH /:id (update), DELETE /:id. "
|
||||
"Alle Routes brauchen Auth (request.jwtVerify()). userId aus JWT-Payload. "
|
||||
"User sieht nur eigene Einträge ausser admin."
|
||||
),
|
||||
deps=["apps/api/src/db/index.ts", "apps/api/src/db/schema.ts", "apps/api/src/routes/auth.ts"],
|
||||
extension="ts",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/api/src/routes/index.ts",
|
||||
purpose=(
|
||||
"Registriert alle Route-Plugins beim Fastify-Server. Importiert authRoutes und timeEntriesRoutes "
|
||||
"und registriert sie. Export: setupRoutes(server: FastifyInstance)."
|
||||
),
|
||||
deps=["apps/api/src/routes/auth.ts", "apps/api/src/routes/time-entries.ts"],
|
||||
extension="ts",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/api/src/index.ts",
|
||||
purpose=(
|
||||
"Fastify-Server-Entry. PORT=4001 oder env.PORT. HOST=0.0.0.0. "
|
||||
"Plugins: @fastify/cors (origin: http://localhost:5174, credentials: true), "
|
||||
"@fastify/cookie, @fastify/jwt (secret aus env JWT_SECRET, fallback 'dev-secret-change-me', cookie: {cookieName: 'token', signed: false}). "
|
||||
"GET /health → {status: 'ok'}. Dann setupRoutes(server). server.listen()."
|
||||
),
|
||||
deps=["apps/api/src/routes/index.ts"],
|
||||
extension="ts",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/web/src/main.tsx",
|
||||
purpose=(
|
||||
"React-Entry. Mountet App in #root. Wrappt mit QueryClientProvider (TanStack Query) und RouterProvider (TanStack Router). "
|
||||
"Importiert ./index.css."
|
||||
),
|
||||
extension="tsx",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/web/src/lib/api.ts",
|
||||
purpose=(
|
||||
"Fetch-Wrapper für die API. Base-URL '/api' (Vite-Proxy auf :4001). "
|
||||
"Funktionen: login(email, password), logout(), getMe(), "
|
||||
"listTimeEntries(opts?), createTimeEntry(data), deleteTimeEntry(id). "
|
||||
"Alle Requests mit credentials: 'include'."
|
||||
),
|
||||
extension="ts",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/web/src/pages/Login.tsx",
|
||||
purpose=(
|
||||
"Login-Page mit Email- und Passwort-Form. Bei Erfolg → navigate('/'). "
|
||||
"Tailwind-Styles, minimalistisch (max-w-md, card, primary-button). "
|
||||
"Default-Werte vorgefüllt: email='admin@emberclone.local', password='emberclone2026' für Dev-Convenience."
|
||||
),
|
||||
deps=["apps/web/src/lib/api.ts"],
|
||||
extension="tsx",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/web/src/pages/Dashboard.tsx",
|
||||
purpose=(
|
||||
"Dashboard mit Welcome-Card ('Hallo {user.name}'), Übersicht über Anzahl heutiger Zeit-Einträge. "
|
||||
"Liest user via getMe() und entries via listTimeEntries(). Logout-Button oben rechts. "
|
||||
"Tailwind, einfaches Grid-Layout."
|
||||
),
|
||||
deps=["apps/web/src/lib/api.ts"],
|
||||
extension="tsx",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/web/src/pages/TimeEntries.tsx",
|
||||
purpose=(
|
||||
"TimeEntries-Page mit Liste aller Einträge des aktuellen Users (TanStack Query) "
|
||||
"und Inline-Form zum Erstellen eines neuen Eintrags (description, startTime, endTime). "
|
||||
"Pro Eintrag: Delete-Button. Tailwind-Tabelle."
|
||||
),
|
||||
deps=["apps/web/src/lib/api.ts"],
|
||||
extension="tsx",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/web/src/App.tsx",
|
||||
purpose=(
|
||||
"Router-Setup mit TanStack Router. Routes: / (Dashboard), /login (Login), /time-entries (TimeEntries). "
|
||||
"Geschützte Routes: bei 401 redirect zu /login. Verwende `createRouter` + `createRootRoute`."
|
||||
),
|
||||
deps=["apps/web/src/pages/Login.tsx", "apps/web/src/pages/Dashboard.tsx", "apps/web/src/pages/TimeEntries.tsx"],
|
||||
extension="tsx",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/web/src/index.css",
|
||||
purpose=(
|
||||
"Tailwind-Entry-CSS: @tailwind base; @tailwind components; @tailwind utilities; "
|
||||
"Plus minimaler Reset für body (bg-slate-50, font-sans)."
|
||||
),
|
||||
extension="css",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/web/postcss.config.cjs",
|
||||
purpose="PostCSS-Config: tailwindcss + autoprefixer als Plugins (CommonJS-Format wegen .cjs).",
|
||||
extension="js",
|
||||
),
|
||||
FileSpec(
|
||||
path="apps/web/tailwind.config.ts",
|
||||
purpose=(
|
||||
"Tailwind-Config v3 mit content: ['./index.html', './src/**/*.{ts,tsx}']. "
|
||||
"Plus theme.extend.colors mit ember: {500: '#f97316', 600: '#ea580c'} als Brand-Color."
|
||||
),
|
||||
extension="ts",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
log_section("EmberClone Codegen-Run gestartet")
|
||||
log(f"Specs: {len(SPECS)} Files zu generieren")
|
||||
log(f"vLLM: {VLLM_URL}, Model: {MODEL}")
|
||||
|
||||
if not BLUEPRINT_PATH.exists():
|
||||
log(f"Blueprint not found at {BLUEPRINT_PATH}", level="ERROR")
|
||||
return 1
|
||||
|
||||
blueprint = BLUEPRINT_PATH.read_text()
|
||||
|
||||
# quick gemma ping
|
||||
log("Pinging Gemma …")
|
||||
pong = await gemma_call('Sag "pong" und sonst nichts.', max_tokens=10)
|
||||
if not pong or "pong" not in pong.lower():
|
||||
log(f"Gemma ping failed: {pong!r}", level="ERROR")
|
||||
return 2
|
||||
log(f"Gemma pong ok: {pong.strip()!r}")
|
||||
|
||||
ok_count = 0
|
||||
fail_count = 0
|
||||
for spec in SPECS:
|
||||
success = await generate_file(spec, blueprint)
|
||||
if success:
|
||||
ok_count += 1
|
||||
# commit per file
|
||||
rc, _ = git("add", spec.path)
|
||||
rc, _ = git("commit", "-q", "-m", f"gemma: generate {spec.path}")
|
||||
else:
|
||||
fail_count += 1
|
||||
# commit anyway so user sees attempt
|
||||
rc, _ = git("add", spec.path)
|
||||
rc, _ = git("commit", "-q", "-m", f"gemma: ATTEMPT {spec.path} (failed validation: {spec.final_error})")
|
||||
|
||||
log_section("Codegen-Run beendet")
|
||||
log(f"ok: {ok_count}/{len(SPECS)}, fail: {fail_count}/{len(SPECS)}")
|
||||
|
||||
if fail_count == 0:
|
||||
log("Alle Files generiert. Triggering Build-Test.")
|
||||
return 0
|
||||
else:
|
||||
log(f"{fail_count} Files mit final-Fehler. Manuelle Inspektion nötig.", level="WARN")
|
||||
return 0 if ok_count > 0 else 3
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(asyncio.run(main()))
|
||||
Loading…
Reference in New Issue
Block a user