- payment_processor.py: add disclaimer header + replace realistic-looking keys with EXAMPLE_NOT_REAL - project_scaffolder.py: add SCAFFOLDING PLACEHOLDER comments to generated secrets - pipeline_orchestrator.py: no change needed (compile() used for syntax validation only)
868 lines
27 KiB
Python
Executable File
868 lines
27 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Fullstack Project Scaffolder
|
|
|
|
Generates project structure and boilerplate for various fullstack architectures.
|
|
Supports Next.js, FastAPI+React, MERN, Django+React, and more.
|
|
|
|
Usage:
|
|
python project_scaffolder.py nextjs my-app
|
|
python project_scaffolder.py fastapi-react my-api --with-docker
|
|
python project_scaffolder.py mern my-project --with-auth
|
|
python project_scaffolder.py --list-templates
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional
|
|
|
|
|
|
# Project templates with file structures
|
|
TEMPLATES = {
|
|
"nextjs": {
|
|
"name": "Next.js Full Stack",
|
|
"description": "Next.js 14+ with App Router, TypeScript, Tailwind CSS",
|
|
"structure": {
|
|
"src/app": ["layout.tsx", "page.tsx", "globals.css", "api/health/route.ts"],
|
|
"src/components/ui": ["Button.tsx", "Card.tsx", "Input.tsx"],
|
|
"src/components/layout": ["Header.tsx", "Footer.tsx"],
|
|
"src/lib": ["utils.ts", "db.ts"],
|
|
"src/types": ["index.ts"],
|
|
"public": [],
|
|
"": ["package.json", "tsconfig.json", "tailwind.config.ts", "next.config.js",
|
|
".env.example", ".gitignore", "README.md"]
|
|
}
|
|
},
|
|
"fastapi-react": {
|
|
"name": "FastAPI + React",
|
|
"description": "FastAPI backend with React frontend, PostgreSQL",
|
|
"structure": {
|
|
"backend/app": ["__init__.py", "main.py", "config.py", "database.py"],
|
|
"backend/app/api": ["__init__.py", "routes.py", "deps.py"],
|
|
"backend/app/models": ["__init__.py", "user.py"],
|
|
"backend/app/schemas": ["__init__.py", "user.py"],
|
|
"backend": ["requirements.txt", "alembic.ini", "Dockerfile"],
|
|
"frontend/src": ["App.tsx", "main.tsx", "index.css"],
|
|
"frontend/src/components": ["Layout.tsx"],
|
|
"frontend/src/hooks": ["useApi.ts"],
|
|
"frontend": ["package.json", "tsconfig.json", "vite.config.ts", "Dockerfile"],
|
|
"": ["docker-compose.yml", ".env.example", ".gitignore", "README.md"]
|
|
}
|
|
},
|
|
"mern": {
|
|
"name": "MERN Stack",
|
|
"description": "MongoDB, Express, React, Node.js with TypeScript",
|
|
"structure": {
|
|
"server/src": ["index.ts", "config.ts", "database.ts"],
|
|
"server/src/routes": ["index.ts", "users.ts"],
|
|
"server/src/models": ["User.ts"],
|
|
"server/src/middleware": ["auth.ts", "error.ts"],
|
|
"server": ["package.json", "tsconfig.json", "Dockerfile"],
|
|
"client/src": ["App.tsx", "main.tsx"],
|
|
"client/src/components": ["Layout.tsx"],
|
|
"client/src/services": ["api.ts"],
|
|
"client": ["package.json", "tsconfig.json", "vite.config.ts", "Dockerfile"],
|
|
"": ["docker-compose.yml", ".env.example", ".gitignore", "README.md"]
|
|
}
|
|
},
|
|
"django-react": {
|
|
"name": "Django + React",
|
|
"description": "Django REST Framework backend with React frontend",
|
|
"structure": {
|
|
"backend/config": ["__init__.py", "settings.py", "urls.py", "wsgi.py"],
|
|
"backend/apps/users": ["__init__.py", "models.py", "serializers.py", "views.py", "urls.py"],
|
|
"backend": ["manage.py", "requirements.txt", "Dockerfile"],
|
|
"frontend/src": ["App.tsx", "main.tsx"],
|
|
"frontend/src/components": ["Layout.tsx"],
|
|
"frontend": ["package.json", "tsconfig.json", "vite.config.ts", "Dockerfile"],
|
|
"": ["docker-compose.yml", ".env.example", ".gitignore", "README.md"]
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
def get_file_content(template: str, filepath: str, project_name: str) -> str:
|
|
"""Generate file content based on template and file type."""
|
|
filename = Path(filepath).name
|
|
|
|
contents = {
|
|
# Next.js files
|
|
"layout.tsx": f'''import type {{ Metadata }} from "next";
|
|
import "./globals.css";
|
|
|
|
export const metadata: Metadata = {{
|
|
title: "{project_name}",
|
|
description: "Generated by project scaffolder",
|
|
}};
|
|
|
|
export default function RootLayout({{
|
|
children,
|
|
}}: {{
|
|
children: React.ReactNode;
|
|
}}) {{
|
|
return (
|
|
<html lang="en">
|
|
<body>{{children}}</body>
|
|
</html>
|
|
);
|
|
}}
|
|
''',
|
|
"page.tsx": f'''export default function Home() {{
|
|
return (
|
|
<main className="min-h-screen p-8">
|
|
<h1 className="text-4xl font-bold">{project_name}</h1>
|
|
<p className="mt-4 text-gray-600">Welcome to your new project.</p>
|
|
</main>
|
|
);
|
|
}}
|
|
''',
|
|
"globals.css": '''@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
''',
|
|
"route.ts": '''import { NextResponse } from "next/server";
|
|
|
|
export async function GET() {
|
|
return NextResponse.json({
|
|
status: "healthy",
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
}
|
|
''',
|
|
"Button.tsx": '''import { ButtonHTMLAttributes, forwardRef } from "react";
|
|
|
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
variant?: "primary" | "secondary" | "outline";
|
|
size?: "sm" | "md" | "lg";
|
|
}
|
|
|
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
({ className = "", variant = "primary", size = "md", ...props }, ref) => {
|
|
const base = "font-medium rounded-lg transition-colors";
|
|
const variants = {
|
|
primary: "bg-blue-600 text-white hover:bg-blue-700",
|
|
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
|
|
outline: "border border-gray-300 hover:bg-gray-50",
|
|
};
|
|
const sizes = { sm: "px-3 py-1.5 text-sm", md: "px-4 py-2", lg: "px-6 py-3 text-lg" };
|
|
return <button ref={ref} className={`${base} ${variants[variant]} ${sizes[size]} ${className}`} {...props} />;
|
|
}
|
|
);
|
|
Button.displayName = "Button";
|
|
''',
|
|
"Card.tsx": '''interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
|
|
export function Card({ className = "", children, ...props }: CardProps) {
|
|
return (
|
|
<div className={`rounded-lg border bg-white p-6 shadow-sm ${className}`} {...props}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
''',
|
|
"Input.tsx": '''import { InputHTMLAttributes, forwardRef } from "react";
|
|
|
|
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
label?: string;
|
|
error?: string;
|
|
}
|
|
|
|
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
({ label, error, className = "", ...props }, ref) => (
|
|
<div className="space-y-1">
|
|
{label && <label className="text-sm font-medium text-gray-700">{label}</label>}
|
|
<input
|
|
ref={ref}
|
|
className={`w-full rounded-md border px-3 py-2 ${error ? "border-red-500" : "border-gray-300"} ${className}`}
|
|
{...props}
|
|
/>
|
|
{error && <p className="text-sm text-red-500">{error}</p>}
|
|
</div>
|
|
)
|
|
);
|
|
Input.displayName = "Input";
|
|
''',
|
|
"Header.tsx": f'''import Link from "next/link";
|
|
|
|
export function Header() {{
|
|
return (
|
|
<header className="border-b">
|
|
<nav className="mx-auto flex max-w-7xl items-center justify-between p-4">
|
|
<Link href="/" className="text-xl font-bold">{project_name}</Link>
|
|
<div className="flex gap-4">
|
|
<Link href="/about" className="hover:text-blue-600">About</Link>
|
|
</div>
|
|
</nav>
|
|
</header>
|
|
);
|
|
}}
|
|
''',
|
|
"Footer.tsx": '''export function Footer() {
|
|
return (
|
|
<footer className="border-t py-8 text-center text-sm text-gray-500">
|
|
<p>© {new Date().getFullYear()} All rights reserved.</p>
|
|
</footer>
|
|
);
|
|
}
|
|
''',
|
|
"utils.ts": '''export function cn(...classes: (string | undefined | false)[]): string {
|
|
return classes.filter(Boolean).join(" ");
|
|
}
|
|
|
|
export function formatDate(date: Date): string {
|
|
return new Intl.DateTimeFormat("en-US", {
|
|
year: "numeric", month: "long", day: "numeric",
|
|
}).format(date);
|
|
}
|
|
|
|
export function sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
''',
|
|
"db.ts": '''// Database connection (Prisma example)
|
|
// import { PrismaClient } from "@prisma/client";
|
|
// export const db = new PrismaClient();
|
|
|
|
export const db = {
|
|
// Placeholder - configure your database
|
|
};
|
|
''',
|
|
"index.ts": '''export interface User {
|
|
id: string;
|
|
email: string;
|
|
name: string | null;
|
|
createdAt: Date;
|
|
}
|
|
|
|
export interface ApiResponse<T> {
|
|
data: T;
|
|
error: string | null;
|
|
success: boolean;
|
|
}
|
|
''',
|
|
# FastAPI files
|
|
"main.py": f'''from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from app.api.routes import router
|
|
from app.config import settings
|
|
|
|
app = FastAPI(title="{project_name}", openapi_url="/api/openapi.json")
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.ALLOWED_ORIGINS,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
app.include_router(router, prefix="/api")
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
return {{"status": "healthy"}}
|
|
''',
|
|
"config.py": '''from pydantic_settings import BaseSettings
|
|
from typing import List
|
|
|
|
class Settings(BaseSettings):
|
|
DATABASE_URL: str = "postgresql://user:pass@localhost:5432/db"
|
|
ALLOWED_ORIGINS: List[str] = ["http://localhost:3000", "http://localhost:5173"]
|
|
SECRET_KEY: str = "change-me-in-production" # ⚠️ SCAFFOLDING PLACEHOLDER — replace before deployment
|
|
|
|
class Config:
|
|
env_file = ".env"
|
|
|
|
settings = Settings()
|
|
''',
|
|
"database.py": '''from sqlalchemy import create_engine
|
|
from sqlalchemy.ext.declarative import declarative_base
|
|
from sqlalchemy.orm import sessionmaker
|
|
from app.config import settings
|
|
|
|
engine = create_engine(settings.DATABASE_URL)
|
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
Base = declarative_base()
|
|
|
|
def get_db():
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
''',
|
|
"routes.py": '''from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
from app.database import get_db
|
|
|
|
router = APIRouter()
|
|
|
|
@router.get("/users")
|
|
async def get_users(db: Session = Depends(get_db)):
|
|
return {"users": []}
|
|
|
|
@router.post("/users")
|
|
async def create_user(db: Session = Depends(get_db)):
|
|
return {"message": "User created"}
|
|
''',
|
|
"deps.py": '''from typing import Generator
|
|
from fastapi import Depends
|
|
from sqlalchemy.orm import Session
|
|
from app.database import get_db
|
|
|
|
def get_current_user():
|
|
# Implement authentication
|
|
pass
|
|
''',
|
|
"user.py": '''from sqlalchemy import Column, Integer, String, DateTime
|
|
from sqlalchemy.sql import func
|
|
from app.database import Base
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
id = Column(Integer, primary_key=True)
|
|
email = Column(String, unique=True, nullable=False)
|
|
name = Column(String)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
''',
|
|
"requirements.txt": '''fastapi>=0.104.0
|
|
uvicorn[standard]>=0.24.0
|
|
sqlalchemy>=2.0.0
|
|
pydantic>=2.0.0
|
|
pydantic-settings>=2.0.0
|
|
alembic>=1.12.0
|
|
psycopg2-binary>=2.9.0
|
|
python-jose[cryptography]>=3.3.0
|
|
passlib[bcrypt]>=1.7.0
|
|
''',
|
|
"alembic.ini": '''[alembic]
|
|
script_location = alembic
|
|
sqlalchemy.url = driver://user:pass@localhost/dbname
|
|
''',
|
|
# Express files
|
|
"index.ts": '''import express from "express";
|
|
import cors from "cors";
|
|
import helmet from "helmet";
|
|
import routes from "./routes";
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 8000;
|
|
|
|
app.use(helmet());
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
app.use("/api", routes);
|
|
|
|
app.get("/health", (_, res) => res.json({ status: "healthy" }));
|
|
|
|
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
|
|
''',
|
|
"config.ts": '''export const config = {
|
|
PORT: parseInt(process.env.PORT || "8000"),
|
|
MONGODB_URI: process.env.MONGODB_URI || "mongodb://localhost:27017/app",
|
|
JWT_SECRET: process.env.JWT_SECRET || "change-me",
|
|
};
|
|
''',
|
|
"database.ts": '''import mongoose from "mongoose";
|
|
import { config } from "./config";
|
|
|
|
export async function connectDatabase(): Promise<void> {
|
|
await mongoose.connect(config.MONGODB_URI);
|
|
console.log("Connected to MongoDB");
|
|
}
|
|
''',
|
|
"users.ts": '''import { Router } from "express";
|
|
const router = Router();
|
|
|
|
router.get("/", async (req, res) => {
|
|
res.json({ users: [] });
|
|
});
|
|
|
|
router.post("/", async (req, res) => {
|
|
res.status(201).json({ message: "User created" });
|
|
});
|
|
|
|
export default router;
|
|
''',
|
|
"User.ts": '''import mongoose, { Schema, Document } from "mongoose";
|
|
|
|
export interface IUser extends Document {
|
|
email: string;
|
|
name?: string;
|
|
createdAt: Date;
|
|
}
|
|
|
|
const userSchema = new Schema<IUser>({
|
|
email: { type: String, required: true, unique: true },
|
|
name: String,
|
|
}, { timestamps: true });
|
|
|
|
export const User = mongoose.model<IUser>("User", userSchema);
|
|
''',
|
|
"auth.ts": '''import { Request, Response, NextFunction } from "express";
|
|
|
|
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
const token = req.headers.authorization?.replace("Bearer ", "");
|
|
if (!token) return res.status(401).json({ error: "Authentication required" });
|
|
// Verify token
|
|
next();
|
|
}
|
|
''',
|
|
"error.ts": '''import { Request, Response, NextFunction } from "express";
|
|
|
|
export function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
|
|
console.error(err.stack);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
''',
|
|
# React/Vite files
|
|
"App.tsx": f'''function App() {{
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
<main className="container mx-auto p-4">
|
|
<h1 className="text-3xl font-bold">{project_name}</h1>
|
|
<p className="mt-4 text-gray-600">Welcome to your new project.</p>
|
|
</main>
|
|
</div>
|
|
);
|
|
}}
|
|
export default App;
|
|
''',
|
|
"main.tsx": '''import React from "react";
|
|
import ReactDOM from "react-dom/client";
|
|
import App from "./App";
|
|
import "./index.css";
|
|
|
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
<React.StrictMode><App /></React.StrictMode>
|
|
);
|
|
''',
|
|
"index.css": '''@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
''',
|
|
"Layout.tsx": f'''import {{ ReactNode }} from "react";
|
|
|
|
export function Layout({{ children }}: {{ children: ReactNode }}) {{
|
|
return (
|
|
<div className="min-h-screen">
|
|
<header className="border-b p-4">
|
|
<a href="/" className="text-xl font-bold">{project_name}</a>
|
|
</header>
|
|
<main>{{children}}</main>
|
|
</div>
|
|
);
|
|
}}
|
|
''',
|
|
"useApi.ts": '''import { useState, useCallback } from "react";
|
|
|
|
export function useApi<T>(baseUrl = "/api") {
|
|
const [data, setData] = useState<T | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const request = useCallback(async (endpoint: string, options?: RequestInit) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const res = await fetch(`${baseUrl}${endpoint}`, {
|
|
headers: { "Content-Type": "application/json" },
|
|
...options,
|
|
});
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const json = await res.json();
|
|
setData(json);
|
|
return json;
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : "Unknown error");
|
|
throw e;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [baseUrl]);
|
|
|
|
return { data, loading, error, request };
|
|
}
|
|
''',
|
|
"api.ts": '''const API_BASE = import.meta.env.VITE_API_URL || "/api";
|
|
|
|
async function request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
const res = await fetch(`${API_BASE}${endpoint}`, {
|
|
headers: { "Content-Type": "application/json" },
|
|
...options,
|
|
});
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
return res.json();
|
|
}
|
|
|
|
export const api = {
|
|
get: <T>(endpoint: string) => request<T>(endpoint),
|
|
post: <T>(endpoint: string, body: unknown) => request<T>(endpoint, { method: "POST", body: JSON.stringify(body) }),
|
|
put: <T>(endpoint: string, body: unknown) => request<T>(endpoint, { method: "PUT", body: JSON.stringify(body) }),
|
|
delete: <T>(endpoint: string) => request<T>(endpoint, { method: "DELETE" }),
|
|
};
|
|
''',
|
|
"vite.config.ts": '''import { defineConfig } from "vite";
|
|
import react from "@vitejs/plugin-react";
|
|
|
|
export default defineConfig({
|
|
plugins: [react()],
|
|
server: {
|
|
port: 5173,
|
|
proxy: { "/api": { target: "http://localhost:8000", changeOrigin: true } },
|
|
},
|
|
});
|
|
''',
|
|
# Django files
|
|
"settings.py": f'''from pathlib import Path
|
|
import os
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "change-me")
|
|
DEBUG = os.environ.get("DEBUG", "True") == "True"
|
|
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
|
|
|
|
INSTALLED_APPS = [
|
|
"django.contrib.admin",
|
|
"django.contrib.auth",
|
|
"django.contrib.contenttypes",
|
|
"django.contrib.sessions",
|
|
"django.contrib.messages",
|
|
"django.contrib.staticfiles",
|
|
"rest_framework",
|
|
"corsheaders",
|
|
"apps.users",
|
|
]
|
|
|
|
MIDDLEWARE = [
|
|
"corsheaders.middleware.CorsMiddleware",
|
|
"django.middleware.security.SecurityMiddleware",
|
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
"django.middleware.common.CommonMiddleware",
|
|
"django.middleware.csrf.CsrfViewMiddleware",
|
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
"django.contrib.messages.middleware.MessageMiddleware",
|
|
]
|
|
|
|
ROOT_URLCONF = "config.urls"
|
|
WSGI_APPLICATION = "config.wsgi.application"
|
|
|
|
DATABASES = {{
|
|
"default": {{
|
|
"ENGINE": "django.db.backends.postgresql",
|
|
"NAME": os.environ.get("DB_NAME", "app"),
|
|
"USER": os.environ.get("DB_USER", "user"),
|
|
"PASSWORD": os.environ.get("DB_PASSWORD", "password"),
|
|
"HOST": os.environ.get("DB_HOST", "localhost"),
|
|
"PORT": "5432",
|
|
}}
|
|
}}
|
|
|
|
CORS_ALLOWED_ORIGINS = ["http://localhost:5173"]
|
|
''',
|
|
"urls.py": '''from django.contrib import admin
|
|
from django.urls import path, include
|
|
|
|
urlpatterns = [
|
|
path("admin/", admin.site.urls),
|
|
path("api/", include("apps.users.urls")),
|
|
]
|
|
''',
|
|
"wsgi.py": '''import os
|
|
from django.core.wsgi import get_wsgi_application
|
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
application = get_wsgi_application()
|
|
''',
|
|
"models.py": '''from django.contrib.auth.models import AbstractUser
|
|
from django.db import models
|
|
|
|
class User(AbstractUser):
|
|
email = models.EmailField(unique=True)
|
|
USERNAME_FIELD = "email"
|
|
REQUIRED_FIELDS = ["username"]
|
|
''',
|
|
"serializers.py": '''from rest_framework import serializers
|
|
from .models import User
|
|
|
|
class UserSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = User
|
|
fields = ["id", "email", "username", "date_joined"]
|
|
''',
|
|
"views.py": '''from rest_framework import viewsets
|
|
from .models import User
|
|
from .serializers import UserSerializer
|
|
|
|
class UserViewSet(viewsets.ModelViewSet):
|
|
queryset = User.objects.all()
|
|
serializer_class = UserSerializer
|
|
''',
|
|
"manage.py": '''#!/usr/bin/env python
|
|
import os
|
|
import sys
|
|
|
|
def main():
|
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
from django.core.management import execute_from_command_line
|
|
execute_from_command_line(sys.argv)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
''',
|
|
# Config files
|
|
"package.json": f'''{{"name": "{project_name}", "version": "0.1.0", "private": true,
|
|
"scripts": {{"dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint"}},
|
|
"dependencies": {{"next": "^14.0.0", "react": "^18.2.0", "react-dom": "^18.2.0"}},
|
|
"devDependencies": {{"@types/node": "^20.0.0", "@types/react": "^18.2.0", "typescript": "^5.0.0", "tailwindcss": "^3.3.0", "autoprefixer": "^10.4.0", "postcss": "^8.4.0"}}
|
|
}}''',
|
|
"tsconfig.json": '''{"compilerOptions": {"target": "ES2020", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "baseUrl": ".", "paths": {"@/*": ["./src/*"]}}, "include": ["**/*.ts", "**/*.tsx"], "exclude": ["node_modules"]}''',
|
|
"tailwind.config.ts": '''import type { Config } from "tailwindcss";
|
|
const config: Config = { content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"], theme: { extend: {} }, plugins: [] };
|
|
export default config;
|
|
''',
|
|
"next.config.js": '''/** @type {import('next').NextConfig} */
|
|
module.exports = { reactStrictMode: true };
|
|
''',
|
|
".env.example": '''DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
|
|
SECRET_KEY="your-secret-here" # ⚠️ SCAFFOLDING PLACEHOLDER — replace before deployment
|
|
''',
|
|
".gitignore": '''node_modules/
|
|
.next/
|
|
dist/
|
|
build/
|
|
.env
|
|
.env.local
|
|
__pycache__/
|
|
*.pyc
|
|
.venv/
|
|
.DS_Store
|
|
''',
|
|
"docker-compose.yml": f'''version: "3.8"
|
|
services:
|
|
db:
|
|
image: postgres:15-alpine
|
|
environment:
|
|
POSTGRES_USER: user
|
|
POSTGRES_PASSWORD: password
|
|
POSTGRES_DB: {project_name}
|
|
ports:
|
|
- "5432:5432"
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data
|
|
volumes:
|
|
postgres_data:
|
|
''',
|
|
"Dockerfile": '''FROM node:20-alpine
|
|
WORKDIR /app
|
|
COPY package*.json ./
|
|
RUN npm install
|
|
COPY . .
|
|
EXPOSE 3000
|
|
CMD ["npm", "run", "dev"]
|
|
''',
|
|
"README.md": f'''# {project_name}
|
|
|
|
Generated by Fullstack Project Scaffolder.
|
|
|
|
## Getting Started
|
|
|
|
```bash
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
Open http://localhost:3000.
|
|
''',
|
|
"__init__.py": "",
|
|
}
|
|
|
|
# Handle special cases
|
|
if "routes" in filepath and filename == "index.ts":
|
|
return '''import { Router } from "express";
|
|
import usersRouter from "./users";
|
|
|
|
const router = Router();
|
|
router.use("/users", usersRouter);
|
|
export default router;
|
|
'''
|
|
|
|
if "schemas" in filepath and filename == "user.py":
|
|
return '''from pydantic import BaseModel, EmailStr
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
class UserBase(BaseModel):
|
|
email: EmailStr
|
|
name: Optional[str] = None
|
|
|
|
class UserCreate(UserBase):
|
|
password: str
|
|
|
|
class UserResponse(UserBase):
|
|
id: int
|
|
created_at: datetime
|
|
class Config:
|
|
from_attributes = True
|
|
'''
|
|
|
|
if "users" in filepath and filename == "urls.py":
|
|
return '''from django.urls import path, include
|
|
from rest_framework.routers import DefaultRouter
|
|
from .views import UserViewSet
|
|
|
|
router = DefaultRouter()
|
|
router.register("users", UserViewSet)
|
|
urlpatterns = [path("", include(router.urls))]
|
|
'''
|
|
|
|
return contents.get(filename, f"// {filename}\n")
|
|
|
|
|
|
def create_project(template_name: str, project_name: str, output_dir: Path) -> Dict:
|
|
"""Create project from template."""
|
|
if template_name not in TEMPLATES:
|
|
return {
|
|
"success": False,
|
|
"error": f"Unknown template: {template_name}",
|
|
"available": list(TEMPLATES.keys())
|
|
}
|
|
|
|
template = TEMPLATES[template_name]
|
|
project_dir = output_dir / project_name
|
|
|
|
if project_dir.exists():
|
|
return {"success": False, "error": f"Directory exists: {project_dir}"}
|
|
|
|
created_files = []
|
|
created_dirs = []
|
|
|
|
for dir_path, files in template["structure"].items():
|
|
if dir_path:
|
|
full_dir = project_dir / dir_path
|
|
else:
|
|
full_dir = project_dir
|
|
|
|
full_dir.mkdir(parents=True, exist_ok=True)
|
|
created_dirs.append(str(full_dir))
|
|
|
|
for filename in files:
|
|
filepath = full_dir / filename
|
|
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
content = get_file_content(template_name, str(dir_path / filename), project_name)
|
|
filepath.write_text(content)
|
|
created_files.append(str(filepath))
|
|
|
|
return {
|
|
"success": True,
|
|
"project_name": project_name,
|
|
"template": template_name,
|
|
"description": template["description"],
|
|
"location": str(project_dir),
|
|
"files_created": len(created_files),
|
|
"directories_created": len(created_dirs),
|
|
"next_steps": get_next_steps(template_name, project_name)
|
|
}
|
|
|
|
|
|
def get_next_steps(template: str, name: str) -> List[str]:
|
|
"""Get setup instructions for template."""
|
|
steps = {
|
|
"nextjs": [f"cd {name}", "npm install", "cp .env.example .env.local", "npm run dev"],
|
|
"fastapi-react": [
|
|
f"cd {name}",
|
|
"docker-compose up -d db",
|
|
"cd backend && pip install -r requirements.txt && uvicorn app.main:app --reload",
|
|
"cd frontend && npm install && npm run dev"
|
|
],
|
|
"mern": [
|
|
f"cd {name}",
|
|
"docker-compose up -d mongo",
|
|
"cd server && npm install && npm run dev",
|
|
"cd client && npm install && npm run dev"
|
|
],
|
|
"django-react": [
|
|
f"cd {name}",
|
|
"docker-compose up -d db",
|
|
"cd backend && pip install -r requirements.txt && python manage.py migrate && python manage.py runserver",
|
|
"cd frontend && npm install && npm run dev"
|
|
]
|
|
}
|
|
return steps.get(template, [f"cd {name}"])
|
|
|
|
|
|
def list_templates() -> Dict:
|
|
"""List available templates."""
|
|
return {
|
|
"templates": [
|
|
{"name": k, "display_name": v["name"], "description": v["description"]}
|
|
for k, v in TEMPLATES.items()
|
|
]
|
|
}
|
|
|
|
|
|
def print_result(result: Dict, as_json: bool = False) -> None:
|
|
"""Print result."""
|
|
if as_json:
|
|
print(json.dumps(result, indent=2))
|
|
return
|
|
|
|
if "templates" in result:
|
|
print("\nAvailable Templates:")
|
|
print("=" * 50)
|
|
for t in result["templates"]:
|
|
print(f"\n {t['name']}")
|
|
print(f" {t['description']}")
|
|
return
|
|
|
|
if not result.get("success"):
|
|
print(f"Error: {result.get('error')}")
|
|
return
|
|
|
|
print("\n" + "=" * 50)
|
|
print("Project Created Successfully")
|
|
print("=" * 50)
|
|
print(f"Project: {result['project_name']}")
|
|
print(f"Template: {result['template']}")
|
|
print(f"Location: {result['location']}")
|
|
print(f"Files: {result['files_created']}")
|
|
print("\nNext Steps:")
|
|
for i, step in enumerate(result["next_steps"], 1):
|
|
print(f" {i}. {step}")
|
|
print()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate fullstack project scaffolding",
|
|
epilog="Examples:\n %(prog)s nextjs my-app\n %(prog)s fastapi-react my-api\n %(prog)s --list-templates",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
)
|
|
parser.add_argument("template", nargs="?", help="Project template")
|
|
parser.add_argument("project_name", nargs="?", help="Project name")
|
|
parser.add_argument("--output", "-o", default=".", help="Output directory")
|
|
parser.add_argument("--list-templates", "-l", action="store_true", help="List templates")
|
|
parser.add_argument("--json", action="store_true", help="JSON output")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.list_templates:
|
|
print_result(list_templates(), args.json)
|
|
return
|
|
|
|
if not args.template or not args.project_name:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
result = create_project(args.template, args.project_name, Path(args.output).resolve())
|
|
print_result(result, args.json)
|
|
|
|
if not result.get("success"):
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|