Spaces:
No application file
No application file
| from __future__ import annotations | |
| import json | |
| import re | |
| from pathlib import Path, PurePosixPath | |
| from typing import Any, Mapping, Sequence | |
| MAX_GENERATED_FILES = 60 | |
| MAX_FILE_SIZE_BYTES = 250 * 1024 | |
| SYSTEM_FILENAMES = { | |
| "README.md", | |
| "PROJECT_EXPLANATION.md", | |
| "SETUP_INSTRUCTIONS.md", | |
| "FILE_STRUCTURE.md", | |
| "PACKAGE_REQUIREMENTS.md", | |
| "REQUIRED_INPUTS.md", | |
| ".env.example", | |
| } | |
| def slugify(value: str, fallback: str = "project-agent-output") -> str: | |
| slug = re.sub(r"[^a-zA-Z0-9]+", "-", value.lower()).strip("-") | |
| return slug or fallback | |
| def sanitize_relative_path(raw_path: str) -> Path: | |
| if not isinstance(raw_path, str): | |
| raise ValueError("Generated file paths must be strings.") | |
| normalized = raw_path.replace("\\", "/").strip() | |
| if not normalized: | |
| raise ValueError("Generated file paths cannot be empty.") | |
| if normalized.startswith(("/", "~")): | |
| raise ValueError(f"Absolute paths are not allowed: {raw_path}") | |
| if re.match(r"^[a-zA-Z]:", normalized): | |
| raise ValueError(f"Drive-qualified paths are not allowed: {raw_path}") | |
| posix_path = PurePosixPath(normalized) | |
| safe_parts: list[str] = [] | |
| for part in posix_path.parts: | |
| if part in {"", "."}: | |
| continue | |
| if part == "..": | |
| raise ValueError(f"Path traversal is not allowed: {raw_path}") | |
| safe_parts.append(part) | |
| if not safe_parts: | |
| raise ValueError(f"Generated file path is invalid: {raw_path}") | |
| return Path(*safe_parts) | |
| def ensure_within_directory(base_dir: Path, candidate: Path) -> bool: | |
| resolved_base = base_dir.resolve() | |
| resolved_candidate = candidate.resolve() | |
| return resolved_candidate.is_relative_to(resolved_base) | |
| def validate_generated_files(files: list[dict[str, str]]) -> list[dict[str, str]]: | |
| if len(files) > MAX_GENERATED_FILES: | |
| raise ValueError( | |
| f"Generated output exceeds the limit of {MAX_GENERATED_FILES} files." | |
| ) | |
| validated: list[dict[str, str]] = [] | |
| seen_paths: set[str] = set() | |
| for file_entry in files: | |
| raw_path = str(file_entry.get("path", "")) | |
| relative_path = sanitize_relative_path(raw_path) | |
| normalized_path = relative_path.as_posix() | |
| if normalized_path in seen_paths: | |
| continue | |
| content = str(file_entry.get("content", "")) | |
| content_size = len(content.encode("utf-8")) | |
| if content_size > MAX_FILE_SIZE_BYTES: | |
| raise ValueError( | |
| f"Generated file '{normalized_path}' exceeds the size limit of 250 KB." | |
| ) | |
| seen_paths.add(normalized_path) | |
| validated.append({"path": normalized_path, "content": content}) | |
| return validated | |
| def build_required_docs( | |
| preview: dict[str, Any], bundle_info: dict[str, Any] | None = None | |
| ) -> dict[str, str]: | |
| bundle_info = bundle_info or {} | |
| project_name = str(preview.get("projectName") or "Generated Project").strip() | |
| detected_choices = _listify(preview.get("detectedUserChoices")) | |
| chosen_stack = _listify(preview.get("chosenStack")) | |
| assumptions = _listify(preview.get("assumptions")) | |
| architecture = _listify(preview.get("architecture")) | |
| package_requirements = _listify(preview.get("packageRequirements")) | |
| install_commands = _listify(preview.get("installCommands")) | |
| run_commands = _listify(preview.get("runCommands")) | |
| modules = preview.get("modules") or [] | |
| required_inputs = _required_input_list(preview.get("requiredInputs")) | |
| env_variables = _env_list(preview.get("envVariables")) or _required_inputs_to_env_list( | |
| required_inputs | |
| ) | |
| file_tree = str( | |
| bundle_info.get("actualFileTree") or preview.get("fileTree") or "" | |
| ).strip() | |
| summary = str(preview.get("summary") or "No summary was provided by the model.").strip() | |
| problem_statement = str( | |
| preview.get("problemStatement") or "No problem statement was provided by the model." | |
| ).strip() | |
| selected_stack = preview.get("selectedStack") or {} | |
| readme = "\n".join( | |
| [ | |
| f"# {project_name}", | |
| "", | |
| summary, | |
| "", | |
| "## What Was Generated", | |
| "This ZIP contains a 100% runnable starter project from the latest preview, including dependency files, setup scripts, run scripts, starter source code, and required input guidance.", | |
| "", | |
| "## Problem Statement", | |
| problem_statement, | |
| "", | |
| "## Selected Stack", | |
| _selected_stack_text(selected_stack), | |
| "", | |
| "## Chosen Stack", | |
| _bullet_text(chosen_stack, "The generated plan did not include stack details."), | |
| "", | |
| "## Detected User Choices", | |
| _bullet_text( | |
| detected_choices, | |
| "The user did not explicitly specify language, tooling, or framework choices.", | |
| ), | |
| "", | |
| "## Architecture Highlights", | |
| _bullet_text(architecture, "Architecture details were not provided."), | |
| "", | |
| "## Core Modules", | |
| _modules_text(modules), | |
| "", | |
| "## Setup", | |
| "Fill `.env` from `.env.example`, then run the setup script before starting the project.", | |
| "- Windows: `setup.bat`", | |
| "- Mac/Linux: `setup.sh`", | |
| "", | |
| "## How To Run", | |
| _bullet_text(run_commands, "No run commands were provided."), | |
| "", | |
| "## Required Inputs", | |
| "Fill these values in `.env` before running the project.", | |
| "", | |
| _required_inputs_summary(required_inputs), | |
| "", | |
| "## Notes", | |
| _bullet_text(assumptions, "No assumptions were recorded."), | |
| ] | |
| ).strip() + "\n" | |
| explanation = "\n".join( | |
| [ | |
| f"# {project_name} Explanation", | |
| "", | |
| "## Summary", | |
| summary, | |
| "", | |
| "## Problem Statement", | |
| problem_statement, | |
| "", | |
| "## Selected Stack", | |
| _selected_stack_text(selected_stack), | |
| "", | |
| "## Architecture", | |
| _bullet_text(architecture, "Architecture details were not provided."), | |
| "", | |
| "## Modules", | |
| _modules_text(modules), | |
| "", | |
| "## Assumptions", | |
| _bullet_text(assumptions, "No assumptions were recorded."), | |
| ] | |
| ).strip() + "\n" | |
| setup_instructions = "\n".join( | |
| [ | |
| "# Setup Instructions", | |
| "", | |
| "## Normal Setup", | |
| "1. Review `README.md`, `PROJECT_EXPLANATION.md`, and `FILE_STRUCTURE.md` first.", | |
| "2. Review `PACKAGE_REQUIREMENTS.md` and `REQUIRED_INPUTS.md` before installing dependencies.", | |
| "3. Copy `.env.example` to `.env` and fill the required values.", | |
| "", | |
| "## Windows", | |
| "1. Fill `.env` from `.env.example`.", | |
| "2. Run `setup.bat`.", | |
| "3. Run `run.bat`.", | |
| "", | |
| "## Mac/Linux", | |
| "1. Fill `.env` from `.env.example`.", | |
| "2. Run `chmod +x setup.sh run.sh`.", | |
| "3. Run `./setup.sh`.", | |
| "4. Run `./run.sh`.", | |
| "", | |
| "## Setup Scripts", | |
| "- Windows: `setup.bat`", | |
| "- Mac/Linux: `setup.sh`", | |
| "", | |
| "## Selected Stack", | |
| _selected_stack_text(selected_stack), | |
| "", | |
| "## Install Commands", | |
| _bullet_text(install_commands, "No install commands were provided."), | |
| "", | |
| "## Run Commands", | |
| _bullet_text(run_commands, "No run commands were provided."), | |
| "", | |
| "## Environment Variables", | |
| _env_variables_text(env_variables), | |
| "", | |
| "## Troubleshooting", | |
| "- If dependencies fail to install, confirm Python, Node.js, or Maven is installed for the selected stack.", | |
| "- If the app cannot connect to a service, double-check the values in `.env` against `REQUIRED_INPUTS.md`.", | |
| "- If frontend and backend both start locally, verify `VITE_API_BASE_URL` or related API host settings match the backend URL.", | |
| "", | |
| "## Notes", | |
| _bullet_text(assumptions, "No additional assumptions were provided."), | |
| ] | |
| ).strip() + "\n" | |
| structure = "\n".join( | |
| [ | |
| "# File Structure", | |
| "", | |
| "## Final Generated Tree", | |
| "```text", | |
| file_tree or "No file tree was available.", | |
| "```", | |
| "", | |
| "## Included Modules", | |
| _modules_text(modules), | |
| ] | |
| ).strip() + "\n" | |
| package_docs = "\n".join( | |
| [ | |
| "# Package Requirements", | |
| "", | |
| "## Libraries And Packages", | |
| _bullet_text(package_requirements, "No package requirements were provided."), | |
| "", | |
| "## Install Commands", | |
| _bullet_text(install_commands, "No install commands were provided."), | |
| "", | |
| "## Run Commands", | |
| _bullet_text(run_commands, "No run commands were provided."), | |
| ] | |
| ).strip() + "\n" | |
| required_inputs_doc = "\n".join( | |
| [ | |
| "# Required Inputs", | |
| "", | |
| "Fill these values in `.env` before running the project.", | |
| "", | |
| "| Name | Required | Example | Where To Add | Purpose |", | |
| "|---|---|---|---|---|", | |
| _required_inputs_table(required_inputs), | |
| ] | |
| ).strip() + "\n" | |
| docs = { | |
| "README.md": readme, | |
| "PROJECT_EXPLANATION.md": explanation, | |
| "SETUP_INSTRUCTIONS.md": setup_instructions, | |
| "FILE_STRUCTURE.md": structure, | |
| "PACKAGE_REQUIREMENTS.md": package_docs, | |
| "REQUIRED_INPUTS.md": required_inputs_doc, | |
| ".env.example": build_env_example(required_inputs), | |
| } | |
| return docs | |
| def build_env_example(required_inputs: list[dict[str, Any]]) -> str: | |
| if not required_inputs: | |
| return ( | |
| "# No required inputs were detected for this starter.\n" | |
| "# Copy this file to .env if you want to override defaults later.\n" | |
| ) | |
| lines = [ | |
| "# Fill these values in .env before running the project.", | |
| "# Copy this file to .env and replace the example values as needed.", | |
| "", | |
| ] | |
| for variable in required_inputs: | |
| name = str(variable.get("name") or "").strip() | |
| example = str(variable.get("example") or "").strip() | |
| if name: | |
| lines.append(f"{name}={example}") | |
| return "\n".join(lines).rstrip() + "\n" | |
| def build_file_tree_from_paths(paths: list[str]) -> str: | |
| if not paths: | |
| return "" | |
| tree: dict[str, Any] = {} | |
| for raw_path in paths: | |
| parts = [part for part in raw_path.replace("\\", "/").split("/") if part] | |
| current = tree | |
| for part in parts: | |
| current = current.setdefault(part, {}) | |
| lines: list[str] = [] | |
| def walk(node: dict[str, Any], depth: int = 0) -> None: | |
| for name, child in sorted(node.items()): | |
| lines.append(f"{' ' * depth}{name}") | |
| walk(child, depth + 1) | |
| walk(tree) | |
| return "\n".join(lines) | |
| def _listify(value: Any) -> list[str]: | |
| if value is None: | |
| return [] | |
| if isinstance(value, str): | |
| items = [line.strip(" -*\t") for line in value.splitlines()] | |
| return [item for item in items if item] | |
| if isinstance(value, dict): | |
| return [ | |
| f"{key}: {str(item).strip()}" | |
| for key, item in value.items() | |
| if str(item).strip() | |
| ] | |
| if isinstance(value, (list, tuple, set)): | |
| items = [] | |
| for item in value: | |
| text = str(item).strip() | |
| if text: | |
| items.append(text) | |
| return items | |
| text = str(value).strip() | |
| return [text] if text else [] | |
| def _env_list(value: Any) -> list[dict[str, str]]: | |
| env_vars: list[dict[str, str]] = [] | |
| if isinstance(value, dict): | |
| value = [ | |
| { | |
| "name": key, | |
| "value": str(item).strip(), | |
| "description": "", | |
| } | |
| for key, item in value.items() | |
| ] | |
| if not isinstance(value, (list, tuple)): | |
| return env_vars | |
| for item in value: | |
| if not isinstance(item, dict): | |
| continue | |
| name = str(item.get("name") or "").strip() | |
| if not name: | |
| continue | |
| env_vars.append( | |
| { | |
| "name": name, | |
| "value": str(item.get("value") or "").strip(), | |
| "description": str(item.get("description") or "").strip(), | |
| } | |
| ) | |
| return env_vars | |
| def _required_input_list(value: Any) -> list[dict[str, Any]]: | |
| if isinstance(value, Mapping): | |
| value = [value] | |
| if not isinstance(value, (list, tuple)): | |
| return [] | |
| required_inputs: list[dict[str, Any]] = [] | |
| for item in value: | |
| if not isinstance(item, Mapping): | |
| continue | |
| name = str(item.get("name") or "").strip() | |
| if not name: | |
| continue | |
| required_inputs.append( | |
| { | |
| "name": name, | |
| "required": bool(item.get("required", True)), | |
| "example": str(item.get("example") or item.get("value") or "").strip(), | |
| "whereToAdd": str(item.get("whereToAdd") or ".env").strip() or ".env", | |
| "purpose": str(item.get("purpose") or item.get("description") or "").strip(), | |
| } | |
| ) | |
| return required_inputs | |
| def _bullet_text(items: list[str], fallback: str) -> str: | |
| if not items: | |
| return f"- {fallback}" | |
| return "\n".join(f"- {item}" for item in items) | |
| def _selected_stack_text(selected_stack: dict[str, Any]) -> str: | |
| if not isinstance(selected_stack, dict) or not selected_stack: | |
| return "- No explicit stack selection was recorded." | |
| ordered_labels = [ | |
| ("language", "Language"), | |
| ("frontend", "Frontend"), | |
| ("backend", "Backend"), | |
| ("database", "Database"), | |
| ("aiTools", "AI / Tools"), | |
| ("deployment", "Deployment"), | |
| ] | |
| lines = [] | |
| for key, label in ordered_labels: | |
| value = str(selected_stack.get(key) or "Auto").strip() | |
| lines.append(f"- {label}: {value}") | |
| return "\n".join(lines) | |
| def _env_variables_text(env_variables: list[dict[str, str]]) -> str: | |
| if not env_variables: | |
| return "- No environment variables are required." | |
| lines = [] | |
| for variable in env_variables: | |
| description = variable.get("description", "").strip() | |
| value = variable.get("value", "").strip() | |
| suffix = f" ({description})" if description else "" | |
| default_text = f" default `{value}`" if value else "" | |
| lines.append(f"- `{variable['name']}`{suffix}{default_text}") | |
| return "\n".join(lines) | |
| def _required_inputs_to_env_list(required_inputs: list[dict[str, Any]]) -> list[dict[str, str]]: | |
| env_vars: list[dict[str, str]] = [] | |
| for item in required_inputs: | |
| env_vars.append( | |
| { | |
| "name": str(item.get("name") or "").strip(), | |
| "value": str(item.get("example") or "").strip(), | |
| "description": str(item.get("purpose") or "").strip(), | |
| } | |
| ) | |
| return [item for item in env_vars if item["name"]] | |
| def _required_inputs_summary(required_inputs: list[dict[str, Any]]) -> str: | |
| if not required_inputs: | |
| return "- No required inputs were detected. `.env.example` is still included for future overrides." | |
| return "\n".join( | |
| f"- `{item['name']}` ({'required' if item.get('required', True) else 'optional'}): {item.get('purpose') or 'No description provided.'}" | |
| for item in required_inputs | |
| ) | |
| def _required_inputs_table(required_inputs: list[dict[str, Any]]) -> str: | |
| if not required_inputs: | |
| return "| None | No | n/a | `.env` | No required external values were detected for this starter. |" | |
| rows = [] | |
| for item in required_inputs: | |
| rows.append( | |
| "| {name} | {required} | {example} | {where_to_add} | {purpose} |".format( | |
| name=item.get("name") or "", | |
| required="Yes" if item.get("required", True) else "No", | |
| example=(item.get("example") or "").replace("|", "\\|"), | |
| where_to_add=(item.get("whereToAdd") or ".env").replace("|", "\\|"), | |
| purpose=(item.get("purpose") or "").replace("|", "\\|"), | |
| ) | |
| ) | |
| return "\n".join(rows) | |
| def _modules_text(modules: list[dict[str, Any]]) -> str: | |
| if not modules: | |
| return "- No modules were provided." | |
| lines: list[str] = [] | |
| for module in modules: | |
| name = str(module.get("name") or "Unnamed module").strip() | |
| purpose = str(module.get("purpose") or "No purpose provided.").strip() | |
| key_files = _listify(module.get("keyFiles")) | |
| lines.append(f"- {name}: {purpose}") | |
| if key_files: | |
| lines.append(f" Key files: {', '.join(key_files)}") | |
| return "\n".join(lines) | |
| MAX_CUSTOM_TEMPLATE_FILES = 8 | |
| MAX_CUSTOM_FILE_LINES = 300 | |
| def finalize_preview_files( | |
| *, | |
| project_name: str, | |
| selected_stack: Mapping[str, Any], | |
| project_kind: Mapping[str, Any], | |
| custom_manifest: Sequence[Mapping[str, Any]] | None = None, | |
| raw_files: Any = None, | |
| ) -> list[dict[str, str]]: | |
| standard_files = _build_standard_files(project_name, selected_stack, project_kind) | |
| custom_template_files = _build_custom_template_files( | |
| custom_manifest or [], | |
| project_name, | |
| selected_stack, | |
| project_kind, | |
| ) | |
| existing_files = _normalize_preview_files(raw_files) | |
| merged_files = _merge_file_entries(standard_files, custom_template_files) | |
| merged_files = _merge_file_entries(merged_files, existing_files) | |
| completed_files = _ensure_minimum_project_files( | |
| merged_files, | |
| project_name, | |
| selected_stack, | |
| project_kind, | |
| ) | |
| repaired_files = _repair_runtime_contract( | |
| completed_files, | |
| project_name, | |
| selected_stack, | |
| project_kind, | |
| ) | |
| return validate_generated_files(repaired_files) | |
| def build_preview_file_tree( | |
| files: Sequence[Mapping[str, Any]], | |
| *, | |
| include_env_example: bool, | |
| ) -> str: | |
| doc_paths = [ | |
| "README.md", | |
| "PROJECT_EXPLANATION.md", | |
| "SETUP_INSTRUCTIONS.md", | |
| "FILE_STRUCTURE.md", | |
| "PACKAGE_REQUIREMENTS.md", | |
| "REQUIRED_INPUTS.md", | |
| ".env.example", | |
| ] | |
| file_paths = [str(file_entry.get("path") or "").strip() for file_entry in files] | |
| return build_file_tree_from_paths([path for path in file_paths if path] + doc_paths) | |
| def _build_standard_files( | |
| project_name: str, | |
| selected_stack: Mapping[str, Any], | |
| project_kind: Mapping[str, Any], | |
| ) -> list[dict[str, str]]: | |
| files: dict[str, str] = {} | |
| if project_kind["isFullStack"]: | |
| if project_kind["hasFrontend"]: | |
| files.update( | |
| _build_frontend_files( | |
| str(selected_stack.get("frontend") or "React"), | |
| project_name, | |
| "frontend", | |
| ) | |
| ) | |
| if project_kind["hasBackend"]: | |
| files.update(_build_backend_files(selected_stack, project_name, "backend")) | |
| files.update(_build_root_scripts(selected_stack, project_kind)) | |
| elif project_kind["hasBackend"]: | |
| files.update(_build_backend_files(selected_stack, project_name, "")) | |
| files.update(_build_root_scripts(selected_stack, project_kind)) | |
| else: | |
| files.update( | |
| _build_frontend_files( | |
| str(selected_stack.get("frontend") or "React"), | |
| project_name, | |
| "", | |
| ) | |
| ) | |
| files.update(_build_root_scripts(selected_stack, project_kind)) | |
| return [{"path": path, "content": content} for path, content in files.items()] | |
| def _build_custom_template_files( | |
| manifest: Sequence[Mapping[str, Any]], | |
| project_name: str, | |
| selected_stack: Mapping[str, Any], | |
| project_kind: Mapping[str, Any], | |
| ) -> list[dict[str, str]]: | |
| files: list[dict[str, str]] = [] | |
| for item in list(manifest)[:MAX_CUSTOM_TEMPLATE_FILES]: | |
| path = _clean_relative_path(item.get("path")) | |
| purpose = str(item.get("purpose") or "").strip() | |
| if not path or not purpose: | |
| continue | |
| content = _build_custom_template_content( | |
| path, | |
| purpose, | |
| project_name, | |
| selected_stack, | |
| project_kind, | |
| ) | |
| files.append({"path": path, "content": _trim_content_lines(content)}) | |
| return files | |
| def _build_custom_template_content( | |
| path: str, | |
| purpose: str, | |
| project_name: str, | |
| selected_stack: Mapping[str, Any], | |
| project_kind: Mapping[str, Any], | |
| ) -> str: | |
| del selected_stack, project_kind | |
| stem = Path(path).stem | |
| pretty_name = stem.replace("_", " ").replace("-", " ").title() | |
| extension = Path(path).suffix.lower() | |
| if extension in {".jsx", ".tsx"}: | |
| if "page" in stem.lower() or "/pages/" in path: | |
| return f"""const cards = [ | |
| {{ | |
| title: "{pretty_name} Overview", | |
| detail: "{purpose}" | |
| }}, | |
| {{ | |
| title: "Starter Workflow", | |
| detail: "Use this page to connect forms, API requests, and user-facing business actions." | |
| }} | |
| ]; | |
| export default function {_safe_component_name(stem)}() {{ | |
| return ( | |
| <section className="card"> | |
| <h2>{pretty_name}</h2> | |
| <p>{purpose}</p> | |
| <ul> | |
| {{cards.map((card) => ( | |
| <li key={{card.title}}> | |
| <strong>{{card.title}}</strong>: {{card.detail}} | |
| </li> | |
| ))}} | |
| </ul> | |
| </section> | |
| ); | |
| }} | |
| """ | |
| return f"""export default function {_safe_component_name(stem)}() {{ | |
| return ( | |
| <section className="card"> | |
| <h3>{pretty_name}</h3> | |
| <p>{purpose}</p> | |
| </section> | |
| ); | |
| }} | |
| """ | |
| if extension == ".py": | |
| if "router" in path or "/routers/" in path: | |
| return f"""from fastapi import APIRouter | |
| router = APIRouter() | |
| @router.get("/") | |
| def read_{_safe_python_name(stem)}() -> dict[str, str]: | |
| return {{ | |
| "message": "{purpose}", | |
| "project": "{project_name}", | |
| }} | |
| """ | |
| if "schema" in path or "/schemas/" in path: | |
| return f"""from pydantic import BaseModel | |
| class {_safe_component_name(stem)}(BaseModel): | |
| name: str | |
| description: str = "{purpose}" | |
| """ | |
| if "model" in path or "/models/" in path: | |
| return f"""from dataclasses import dataclass | |
| @dataclass | |
| class {_safe_component_name(stem)}: | |
| name: str | |
| status: str = "ready" | |
| """ | |
| return f"""def {_safe_python_name(stem)}_summary() -> dict[str, str]: | |
| return {{ | |
| "name": "{pretty_name}", | |
| "purpose": "{purpose}", | |
| "project": "{project_name}", | |
| }} | |
| """ | |
| if extension in {".js", ".mjs"}: | |
| if "service" in path.lower(): | |
| return f"""export function get{_safe_component_name(stem)}Summary() {{ | |
| return {{ | |
| project: "{project_name}", | |
| purpose: "{purpose}" | |
| }}; | |
| }} | |
| """ | |
| if "controller" in path.lower(): | |
| return f"""export function {_safe_js_name(stem)}(_req, res) {{ | |
| res.json({{ | |
| project: "{project_name}", | |
| purpose: "{purpose}" | |
| }}); | |
| }} | |
| """ | |
| return f"""export const {_safe_js_name(stem)} = {{ | |
| project: "{project_name}", | |
| purpose: "{purpose}" | |
| }}; | |
| """ | |
| if extension == ".java": | |
| class_name = _safe_component_name(stem) | |
| return f"""package com.example.demo.service; | |
| import org.springframework.stereotype.Service; | |
| @Service | |
| public class {class_name} {{ | |
| public String summary() {{ | |
| return "{purpose}"; | |
| }} | |
| }} | |
| """ | |
| return f"# {pretty_name}\n\n{purpose}\n" | |
| def _ensure_minimum_project_files( | |
| files: Sequence[Mapping[str, str]], | |
| project_name: str, | |
| selected_stack: Mapping[str, Any], | |
| project_kind: Mapping[str, Any], | |
| ) -> list[dict[str, str]]: | |
| merged = {str(entry["path"]): str(entry["content"]) for entry in files} | |
| for path, content in _build_root_scripts(selected_stack, project_kind).items(): | |
| merged.setdefault(path, content) | |
| if project_kind["isFullStack"]: | |
| for path, content in _build_frontend_files( | |
| str(selected_stack.get("frontend") or "React"), | |
| project_name, | |
| "frontend", | |
| ).items(): | |
| merged.setdefault(path, content) | |
| for path, content in _build_backend_files(selected_stack, project_name, "backend").items(): | |
| merged.setdefault(path, content) | |
| elif project_kind["hasBackend"]: | |
| for path, content in _build_backend_files(selected_stack, project_name, "").items(): | |
| merged.setdefault(path, content) | |
| else: | |
| for path, content in _build_frontend_files( | |
| str(selected_stack.get("frontend") or "React"), | |
| project_name, | |
| "", | |
| ).items(): | |
| merged.setdefault(path, content) | |
| filler_index = 1 | |
| minimum_files = int(project_kind.get("minimumFiles") or 0) | |
| while len(merged) < minimum_files: | |
| filler_path = f"notes/starter-note-{filler_index}.md" | |
| merged.setdefault( | |
| filler_path, | |
| f"# Starter Note {filler_index}\n\nThis file preserves the complete minimum project structure while you continue iterating.\n", | |
| ) | |
| filler_index += 1 | |
| return [{"path": path, "content": content} for path, content in merged.items()] | |
| def _repair_runtime_contract( | |
| files: Sequence[Mapping[str, str]], | |
| project_name: str, | |
| selected_stack: Mapping[str, Any], | |
| project_kind: Mapping[str, Any], | |
| ) -> list[dict[str, str]]: | |
| standard_map = { | |
| item["path"]: item["content"] | |
| for item in _build_standard_files(project_name, selected_stack, project_kind) | |
| } | |
| merged = {str(entry["path"]): str(entry["content"]) for entry in files} | |
| for protected_path in _protected_runtime_paths(selected_stack, project_kind): | |
| template_content = standard_map.get(protected_path) | |
| if template_content is not None: | |
| merged[protected_path] = template_content | |
| for required_path in _required_runtime_paths(selected_stack, project_kind): | |
| template_content = standard_map.get(required_path) | |
| if template_content is None: | |
| continue | |
| if not str(merged.get(required_path, "")).strip(): | |
| merged[required_path] = template_content | |
| for path, content in list(merged.items()): | |
| if not str(content).strip(): | |
| replacement = standard_map.get(path) or _build_safe_fallback_content( | |
| path, | |
| project_name, | |
| ) | |
| merged[path] = replacement | |
| for path, content in list(merged.items()): | |
| if not path.endswith(".py"): | |
| continue | |
| if _python_compiles(content): | |
| continue | |
| merged[path] = standard_map.get(path) or _build_safe_fallback_content(path, project_name) | |
| for package_json_path in _package_json_paths(selected_stack, project_kind): | |
| if package_json_path not in merged or not _valid_package_json( | |
| merged[package_json_path], | |
| _expected_package_scripts(package_json_path, selected_stack, project_kind), | |
| ): | |
| template = standard_map.get(package_json_path) | |
| if template is not None: | |
| merged[package_json_path] = template | |
| for entry_path in _entry_validation_paths(selected_stack, project_kind): | |
| template = standard_map.get(entry_path) | |
| content = str(merged.get(entry_path, "")) | |
| if template is None: | |
| continue | |
| if not content.strip() or not _valid_entry_file(entry_path, content): | |
| merged[entry_path] = template | |
| if project_kind["hasBackend"]: | |
| for endpoint_path in _backend_endpoint_paths(selected_stack, project_kind): | |
| template = standard_map.get(endpoint_path) | |
| if template is None: | |
| continue | |
| content = str(merged.get(endpoint_path, "")) | |
| if not content.strip() or not _valid_backend_endpoint_file(endpoint_path, content): | |
| merged[endpoint_path] = template | |
| if project_kind["hasFrontend"]: | |
| for page_path in _frontend_page_paths(selected_stack, project_kind): | |
| template = standard_map.get(page_path) | |
| if template is None: | |
| continue | |
| if not str(merged.get(page_path, "")).strip(): | |
| merged[page_path] = template | |
| return [{"path": path, "content": content} for path, content in merged.items()] | |
| def _build_backend_files( | |
| selected_stack: Mapping[str, Any], | |
| project_name: str, | |
| prefix: str, | |
| ) -> dict[str, str]: | |
| backend = str(selected_stack.get("backend") or "FastAPI") | |
| if backend in {"FastAPI", "Flask"}: | |
| return _build_fastapi_backend_files(project_name, prefix) | |
| if backend in {"Express", "NestJS"}: | |
| return _build_express_backend_files(project_name, prefix) | |
| if backend == "Spring Boot": | |
| return _build_spring_backend_files(project_name, prefix) | |
| return _build_fastapi_backend_files(project_name, prefix) | |
| def _build_frontend_files(frontend: str, project_name: str, prefix: str) -> dict[str, str]: | |
| if frontend in {"React", "Next.js", "Vue"}: | |
| return _build_react_frontend_files(project_name, prefix) | |
| return _build_vanilla_frontend_files(project_name, prefix) | |
| def _build_fastapi_backend_files(project_name: str, prefix: str) -> dict[str, str]: | |
| app_prefix = _prefixed(prefix, "app") | |
| return { | |
| _prefixed(prefix, "requirements.txt"): "\n".join( | |
| [ | |
| "fastapi", | |
| "uvicorn[standard]", | |
| "pydantic", | |
| "pydantic-settings", | |
| "sqlalchemy", | |
| "python-dotenv", | |
| "aiosqlite", | |
| "", | |
| ] | |
| ), | |
| _prefixed(app_prefix, "__init__.py"): "", | |
| _prefixed(app_prefix, "main.py"): f"""from fastapi import FastAPI | |
| from app.routers import health, items | |
| app = FastAPI(title="{project_name} API") | |
| app.include_router(health.router) | |
| app.include_router(items.router, prefix="/api/items", tags=["items"]) | |
| @app.get("/") | |
| def read_root() -> dict[str, str]: | |
| return {{"status": "ok", "message": "Project is running"}} | |
| """, | |
| _prefixed(app_prefix, "routers/__init__.py"): "", | |
| _prefixed(app_prefix, "routers/health.py"): """from fastapi import APIRouter | |
| from app.schemas.health import HealthResponse | |
| router = APIRouter(tags=["health"]) | |
| @router.get("/health", response_model=HealthResponse) | |
| def healthcheck() -> HealthResponse: | |
| return HealthResponse(status="ok", message="Project is running") | |
| """, | |
| _prefixed(app_prefix, "routers/items.py"): """from fastapi import APIRouter | |
| from app.schemas.item import Item | |
| from app.services.item_service import list_items | |
| router = APIRouter() | |
| @router.get("/", response_model=list[Item]) | |
| def get_items() -> list[Item]: | |
| return list_items() | |
| """, | |
| _prefixed(app_prefix, "services/__init__.py"): "", | |
| _prefixed(app_prefix, "services/app_service.py"): f"""def get_app_summary() -> str: | |
| return "{project_name} includes routes, services, schemas, and configuration for quick iteration." | |
| """, | |
| _prefixed(app_prefix, "services/item_service.py"): """from app.schemas.item import Item | |
| def list_items() -> list[Item]: | |
| return [ | |
| Item(id=1, name="Starter task", status="ready"), | |
| Item(id=2, name="Next iteration", status="planned"), | |
| ] | |
| """, | |
| _prefixed(app_prefix, "models/__init__.py"): "", | |
| _prefixed(app_prefix, "models/base.py"): """from sqlalchemy.orm import DeclarativeBase | |
| class Base(DeclarativeBase): | |
| pass | |
| """, | |
| _prefixed(app_prefix, "models/item.py"): """from sqlalchemy import String | |
| from sqlalchemy.orm import Mapped, mapped_column | |
| from app.models.base import Base | |
| class ItemModel(Base): | |
| __tablename__ = "items" | |
| id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) | |
| name: Mapped[str] = mapped_column(String(120)) | |
| status: Mapped[str] = mapped_column(String(40), default="ready") | |
| """, | |
| _prefixed(app_prefix, "schemas/__init__.py"): "", | |
| _prefixed(app_prefix, "schemas/health.py"): """from pydantic import BaseModel | |
| class HealthResponse(BaseModel): | |
| status: str | |
| message: str | |
| """, | |
| _prefixed(app_prefix, "schemas/item.py"): """from pydantic import BaseModel | |
| class Item(BaseModel): | |
| id: int | |
| name: str | |
| status: str | |
| """, | |
| _prefixed(app_prefix, "database.py"): """from sqlalchemy import create_engine | |
| from sqlalchemy.orm import sessionmaker | |
| from app.config import settings | |
| engine = create_engine(settings.database_url, future=True) | |
| SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) | |
| """, | |
| _prefixed(app_prefix, "config.py"): """from pydantic_settings import BaseSettings, SettingsConfigDict | |
| class Settings(BaseSettings): | |
| model_config = SettingsConfigDict(env_file=".env") | |
| app_env: str = "development" | |
| database_url: str = "sqlite:///./app.db" | |
| settings = Settings() | |
| """, | |
| } | |
| def _build_express_backend_files(project_name: str, prefix: str) -> dict[str, str]: | |
| return { | |
| _prefixed(prefix, "package.json"): json.dumps( | |
| { | |
| "name": project_name.lower().replace(" ", "-"), | |
| "version": "0.1.0", | |
| "private": True, | |
| "type": "module", | |
| "scripts": {"dev": "node --watch server.js", "start": "node server.js"}, | |
| "dependencies": {"cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2"}, | |
| }, | |
| indent=2, | |
| ) | |
| + "\n", | |
| _prefixed(prefix, "server.js"): f"""import cors from "cors"; | |
| import dotenv from "dotenv"; | |
| import express from "express"; | |
| import indexRouter from "./src/routes/index.js"; | |
| import itemsRouter from "./src/routes/items.js"; | |
| dotenv.config(); | |
| const app = express(); | |
| app.use(cors()); | |
| app.use(express.json()); | |
| app.use("/", indexRouter); | |
| app.use("/api/items", itemsRouter); | |
| const port = process.env.PORT || 8000; | |
| app.listen(port, () => {{ | |
| console.log("{project_name} API listening on port", port); | |
| }}); | |
| """, | |
| _prefixed(prefix, "src/routes/index.js"): """import { Router } from "express"; | |
| import { getStatus } from "../controllers/appController.js"; | |
| const router = Router(); | |
| router.get("/", getStatus); | |
| export default router; | |
| """, | |
| _prefixed(prefix, "src/routes/items.js"): """import { Router } from "express"; | |
| import { listItems } from "../controllers/itemController.js"; | |
| const router = Router(); | |
| router.get("/", listItems); | |
| export default router; | |
| """, | |
| _prefixed(prefix, "src/controllers/appController.js"): """export function getStatus(_req, res) { | |
| res.json({ status: "ok", message: "Project is running" }); | |
| } | |
| """, | |
| _prefixed(prefix, "src/controllers/itemController.js"): """import { getItems } from "../services/itemService.js"; | |
| export function listItems(_req, res) { | |
| res.json(getItems()); | |
| } | |
| """, | |
| _prefixed(prefix, "src/services/appService.js"): f"""export function getAppSummary() {{ | |
| return "{project_name} includes routes, controllers, services, and starter configuration."; | |
| }} | |
| """, | |
| _prefixed(prefix, "src/services/itemService.js"): """export function getItems() { | |
| return [ | |
| { id: 1, name: "Starter task", status: "ready" }, | |
| { id: 2, name: "Next iteration", status: "planned" } | |
| ]; | |
| } | |
| """, | |
| _prefixed(prefix, "src/models/itemModel.js"): """export const itemShape = { | |
| id: "number", | |
| name: "string", | |
| status: "string" | |
| }; | |
| """, | |
| } | |
| def _build_react_frontend_files(project_name: str, prefix: str) -> dict[str, str]: | |
| return { | |
| _prefixed(prefix, "package.json"): json.dumps( | |
| { | |
| "name": project_name.lower().replace(" ", "-") + "-frontend", | |
| "private": True, | |
| "version": "0.1.0", | |
| "type": "module", | |
| "scripts": {"dev": "vite", "build": "vite build", "preview": "vite preview"}, | |
| "dependencies": {"react": "^18.3.1", "react-dom": "^18.3.1"}, | |
| "devDependencies": {"vite": "^5.4.0", "@vitejs/plugin-react": "^4.3.1"}, | |
| }, | |
| indent=2, | |
| ) | |
| + "\n", | |
| _prefixed(prefix, "index.html"): """<!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Project Starter</title> | |
| <script type="module" src="/src/main.jsx"></script> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| </body> | |
| </html> | |
| """, | |
| _prefixed(prefix, "vite.config.js"): """import { defineConfig } from "vite"; | |
| import react from "@vitejs/plugin-react"; | |
| export default defineConfig({ | |
| plugins: [react()], | |
| server: { | |
| port: 5173 | |
| } | |
| }); | |
| """, | |
| _prefixed(prefix, "src/main.jsx"): """import React from "react"; | |
| import ReactDOM from "react-dom/client"; | |
| import App from "./App"; | |
| import "./styles.css"; | |
| ReactDOM.createRoot(document.getElementById("root")).render( | |
| <React.StrictMode> | |
| <App /> | |
| </React.StrictMode> | |
| ); | |
| """, | |
| _prefixed(prefix, "src/App.jsx"): f"""import AppShell from "./components/AppShell"; | |
| import HomePage from "./pages/HomePage"; | |
| export default function App() {{ | |
| return ( | |
| <AppShell title="{project_name}"> | |
| <HomePage /> | |
| </AppShell> | |
| ); | |
| }} | |
| """, | |
| _prefixed(prefix, "src/components/AppShell.jsx"): """export default function AppShell({ title, children }) { | |
| return ( | |
| <div className="app-shell"> | |
| <header className="hero"> | |
| <p className="eyebrow">Generated by Project Agent</p> | |
| <h1>{title}</h1> | |
| </header> | |
| <main>{children}</main> | |
| </div> | |
| ); | |
| } | |
| """, | |
| _prefixed(prefix, "src/pages/HomePage.jsx"): """import { getProjectHealth } from "../services/api"; | |
| export default function HomePage() { | |
| const projectHealth = getProjectHealth(); | |
| return ( | |
| <section className="card"> | |
| <h2>Starter Overview</h2> | |
| <p>This 100% runnable starter project is ready for your first feature slice.</p> | |
| <p>API health source: {projectHealth}</p> | |
| </section> | |
| ); | |
| } | |
| """, | |
| _prefixed(prefix, "src/services/api.js"): """export function getProjectHealth() { | |
| return import.meta.env.VITE_API_BASE_URL || "http://localhost:8000"; | |
| } | |
| """, | |
| _prefixed(prefix, "src/styles.css"): """:root { | |
| color-scheme: light; | |
| font-family: "Segoe UI", Arial, sans-serif; | |
| background: #f5f7fb; | |
| color: #132238; | |
| } | |
| body { | |
| margin: 0; | |
| } | |
| .app-shell { | |
| max-width: 960px; | |
| margin: 0 auto; | |
| padding: 32px 20px 56px; | |
| } | |
| .hero { | |
| margin-bottom: 24px; | |
| } | |
| .eyebrow { | |
| font-size: 0.75rem; | |
| letter-spacing: 0.12em; | |
| text-transform: uppercase; | |
| color: #57657d; | |
| } | |
| .card { | |
| background: white; | |
| border-radius: 16px; | |
| padding: 24px; | |
| box-shadow: 0 18px 40px rgba(19, 34, 56, 0.08); | |
| } | |
| """, | |
| } | |
| def _build_vanilla_frontend_files(project_name: str, prefix: str) -> dict[str, str]: | |
| return { | |
| _prefixed(prefix, "package.json"): json.dumps( | |
| { | |
| "name": project_name.lower().replace(" ", "-") + "-frontend", | |
| "private": True, | |
| "version": "0.1.0", | |
| "type": "module", | |
| "scripts": {"dev": "vite", "build": "vite build", "preview": "vite preview"}, | |
| "devDependencies": {"vite": "^5.4.0"}, | |
| }, | |
| indent=2, | |
| ) | |
| + "\n", | |
| _prefixed(prefix, "index.html"): """<!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Project Starter</title> | |
| <script type="module" src="/src/main.js"></script> | |
| </head> | |
| <body> | |
| <div id="app"></div> | |
| </body> | |
| </html> | |
| """, | |
| _prefixed(prefix, "vite.config.js"): """import { defineConfig } from "vite"; | |
| export default defineConfig({ | |
| server: { | |
| port: 5173 | |
| } | |
| }); | |
| """, | |
| _prefixed(prefix, "src/main.js"): f"""import {{ renderHomePage }} from "./views/home.js"; | |
| import "./styles.css"; | |
| document.querySelector("#app").innerHTML = renderHomePage("{project_name}"); | |
| """, | |
| _prefixed(prefix, "src/views/home.js"): """export function renderHomePage(title) { | |
| return ` | |
| <main class="app-shell"> | |
| <section class="card"> | |
| <p class="eyebrow">Generated by Project Agent</p> | |
| <h1>${title}</h1> | |
| <p>This starter is ready for your first feature slice.</p> | |
| </section> | |
| </main> | |
| `; | |
| } | |
| """, | |
| _prefixed(prefix, "src/services/api.js"): """export function getApiBaseUrl() { | |
| return import.meta.env.VITE_API_BASE_URL || "http://localhost:8000"; | |
| } | |
| """, | |
| _prefixed(prefix, "src/styles.css"): """body { | |
| margin: 0; | |
| font-family: "Segoe UI", Arial, sans-serif; | |
| background: #f5f7fb; | |
| color: #132238; | |
| } | |
| .app-shell { | |
| max-width: 960px; | |
| margin: 0 auto; | |
| padding: 32px 20px 56px; | |
| } | |
| .card { | |
| background: white; | |
| border-radius: 16px; | |
| padding: 24px; | |
| box-shadow: 0 18px 40px rgba(19, 34, 56, 0.08); | |
| } | |
| .eyebrow { | |
| font-size: 0.75rem; | |
| text-transform: uppercase; | |
| color: #57657d; | |
| } | |
| """, | |
| } | |
| def _build_spring_backend_files(project_name: str, prefix: str) -> dict[str, str]: | |
| java_base = _prefixed(prefix, "src/main/java/com/example/demo") | |
| resources_base = _prefixed(prefix, "src/main/resources") | |
| return { | |
| _prefixed(prefix, "pom.xml"): """<project xmlns="http://maven.apache.org/POM/4.0.0" | |
| xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |
| <modelVersion>4.0.0</modelVersion> | |
| <groupId>com.example</groupId> | |
| <artifactId>demo</artifactId> | |
| <version>0.0.1-SNAPSHOT</version> | |
| <parent> | |
| <groupId>org.springframework.boot</groupId> | |
| <artifactId>spring-boot-starter-parent</artifactId> | |
| <version>3.3.2</version> | |
| </parent> | |
| <properties> | |
| <java.version>17</java.version> | |
| </properties> | |
| <dependencies> | |
| <dependency> | |
| <groupId>org.springframework.boot</groupId> | |
| <artifactId>spring-boot-starter-web</artifactId> | |
| </dependency> | |
| <dependency> | |
| <groupId>org.springframework.boot</groupId> | |
| <artifactId>spring-boot-starter-data-jpa</artifactId> | |
| </dependency> | |
| </dependencies> | |
| <build> | |
| <plugins> | |
| <plugin> | |
| <groupId>org.springframework.boot</groupId> | |
| <artifactId>spring-boot-maven-plugin</artifactId> | |
| </plugin> | |
| </plugins> | |
| </build> | |
| </project> | |
| """, | |
| _prefixed(java_base, "Application.java"): """package com.example.demo; | |
| import org.springframework.boot.SpringApplication; | |
| import org.springframework.boot.autoconfigure.SpringBootApplication; | |
| @SpringBootApplication | |
| public class Application { | |
| public static void main(String[] args) { | |
| SpringApplication.run(Application.class, args); | |
| } | |
| } | |
| """, | |
| _prefixed(java_base, "controller/AppController.java"): """package com.example.demo.controller; | |
| import com.example.demo.service.AppService; | |
| import java.util.Map; | |
| import org.springframework.web.bind.annotation.GetMapping; | |
| import org.springframework.web.bind.annotation.RestController; | |
| @RestController | |
| public class AppController { | |
| private final AppService appService; | |
| public AppController(AppService appService) { | |
| this.appService = appService; | |
| } | |
| @GetMapping("/") | |
| public Map<String, String> status() { | |
| return Map.of("status", "ok", "message", "Project is running"); | |
| } | |
| } | |
| """, | |
| _prefixed(java_base, "service/AppService.java"): f"""package com.example.demo.service; | |
| import org.springframework.stereotype.Service; | |
| @Service | |
| public class AppService {{ | |
| public String status() {{ | |
| return "Project is running"; | |
| }} | |
| }} | |
| """, | |
| _prefixed(java_base, "model/AppModel.java"): """package com.example.demo.model; | |
| public class AppModel { | |
| private Long id; | |
| private String name; | |
| public Long getId() { return id; } | |
| public void setId(Long id) { this.id = id; } | |
| public String getName() { return name; } | |
| public void setName(String name) { this.name = name; } | |
| } | |
| """, | |
| _prefixed(java_base, "repository/AppRepository.java"): """package com.example.demo.repository; | |
| import com.example.demo.model.AppModel; | |
| import java.util.List; | |
| import org.springframework.stereotype.Repository; | |
| @Repository | |
| public class AppRepository { | |
| public List<AppModel> findAll() { | |
| return List.of(); | |
| } | |
| } | |
| """, | |
| _prefixed(resources_base, "application.properties"): """spring.application.name=demo | |
| server.port=8080 | |
| """, | |
| } | |
| def _build_root_scripts( | |
| selected_stack: Mapping[str, Any], | |
| project_kind: Mapping[str, Any], | |
| ) -> dict[str, str]: | |
| if project_kind["isFullStack"]: | |
| return _build_fullstack_scripts(selected_stack) | |
| if project_kind["hasBackend"]: | |
| backend = str(selected_stack.get("backend") or "FastAPI") | |
| if backend in {"FastAPI", "Flask"}: | |
| return _build_python_scripts(".") | |
| if backend in {"Express", "NestJS"}: | |
| return _build_node_scripts(".", "start") | |
| if backend == "Spring Boot": | |
| return _build_java_scripts(".") | |
| return _build_node_scripts(".", "dev") | |
| def _build_fullstack_scripts(selected_stack: Mapping[str, Any]) -> dict[str, str]: | |
| backend_setup = "" | |
| backend_run_windows = "echo No backend runtime configured.\n" | |
| backend_run_unix = 'echo "No backend runtime configured."\n' | |
| backend = str(selected_stack.get("backend") or "") | |
| if backend in {"FastAPI", "Flask"}: | |
| backend_setup = ( | |
| "if exist backend\\requirements.txt (\n" | |
| " pushd backend\n" | |
| " python -m venv .venv\n" | |
| " call .venv\\Scripts\\python -m pip install --upgrade pip\n" | |
| " call .venv\\Scripts\\pip install -r requirements.txt\n" | |
| " popd\n" | |
| ")\n" | |
| ) | |
| backend_run_windows = ( | |
| 'start "Backend" cmd /k "cd backend && .venv\\Scripts\\python -m uvicorn app.main:app --reload"\n' | |
| ) | |
| backend_run_unix = '(cd backend && . .venv/bin/activate && uvicorn app.main:app --reload) &\n' | |
| elif backend in {"Express", "NestJS"}: | |
| backend_setup = ( | |
| "if exist backend\\package.json (\n" | |
| " pushd backend\n" | |
| " call npm install\n" | |
| " popd\n" | |
| ")\n" | |
| ) | |
| backend_run_windows = 'start "Backend" cmd /k "cd backend && npm start"\n' | |
| backend_run_unix = '(cd backend && npm start) &\n' | |
| elif backend == "Spring Boot": | |
| backend_setup = ( | |
| "where mvn >nul 2>nul && (\n" | |
| " pushd backend\n" | |
| " call mvn install\n" | |
| " popd\n" | |
| ") || echo Maven not found. Skipping backend install.\n" | |
| ) | |
| backend_run_windows = 'start "Backend" cmd /k "cd backend && mvn spring-boot:run"\n' | |
| backend_run_unix = '(cd backend && mvn spring-boot:run) &\n' | |
| return { | |
| "setup.bat": f"""@echo off | |
| setlocal | |
| {backend_setup}if exist frontend\\package.json ( | |
| pushd frontend | |
| call npm install | |
| popd | |
| ) | |
| echo Setup complete. | |
| """, | |
| "setup.sh": """#!/usr/bin/env bash | |
| set -e | |
| if [ -f backend/requirements.txt ]; then | |
| ( | |
| cd backend | |
| python3 -m venv .venv | |
| . .venv/bin/activate | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt | |
| ) | |
| fi | |
| if [ -f backend/package.json ]; then | |
| (cd backend && npm install) | |
| fi | |
| if [ -f backend/pom.xml ]; then | |
| if command -v mvn >/dev/null 2>&1; then | |
| (cd backend && mvn install) | |
| else | |
| echo "Maven not found. Skipping backend install." | |
| fi | |
| fi | |
| if [ -f frontend/package.json ]; then | |
| (cd frontend && npm install) | |
| fi | |
| echo "Setup complete." | |
| """, | |
| "run.bat": f"""@echo off | |
| setlocal | |
| {backend_run_windows}start "Frontend" cmd /k "cd frontend && npm run dev" | |
| """, | |
| "run.sh": f"""#!/usr/bin/env bash | |
| set -e | |
| {backend_run_unix}(cd frontend && npm run dev) & | |
| wait | |
| """, | |
| } | |
| def _build_python_scripts(target_dir: str) -> dict[str, str]: | |
| del target_dir | |
| return { | |
| "setup.bat": """@echo off | |
| setlocal | |
| python -m venv .venv | |
| call .venv\\Scripts\\python -m pip install --upgrade pip | |
| call .venv\\Scripts\\pip install -r requirements.txt | |
| echo Setup complete. | |
| """, | |
| "setup.sh": """#!/usr/bin/env bash | |
| set -e | |
| python3 -m venv .venv | |
| . .venv/bin/activate | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt | |
| echo "Setup complete." | |
| """, | |
| "run.bat": """@echo off | |
| setlocal | |
| call .venv\\Scripts\\python -m uvicorn app.main:app --reload | |
| """, | |
| "run.sh": """#!/usr/bin/env bash | |
| set -e | |
| . .venv/bin/activate | |
| uvicorn app.main:app --reload | |
| """, | |
| } | |
| def _build_node_scripts(target_dir: str, script_name: str) -> dict[str, str]: | |
| directory_prefix = "" if target_dir in {"", "."} else f"{target_dir}/" | |
| windows_prefix = "" if target_dir in {"", "."} else f"{target_dir}\\" | |
| run_script = "npm start" if script_name == "start" else "npm run dev" | |
| return { | |
| "setup.bat": f"""@echo off | |
| setlocal | |
| pushd {windows_prefix or "."} | |
| call npm install | |
| popd | |
| echo Setup complete. | |
| """, | |
| "setup.sh": f"""#!/usr/bin/env bash | |
| set -e | |
| (cd {directory_prefix or "."} && npm install) | |
| echo "Setup complete." | |
| """, | |
| "run.bat": f"""@echo off | |
| setlocal | |
| pushd {windows_prefix or "."} | |
| call {run_script} | |
| popd | |
| """, | |
| "run.sh": f"""#!/usr/bin/env bash | |
| set -e | |
| (cd {directory_prefix or "."} && {run_script}) | |
| """, | |
| } | |
| def _protected_runtime_paths( | |
| selected_stack: Mapping[str, Any], | |
| project_kind: Mapping[str, Any], | |
| ) -> set[str]: | |
| paths: set[str] = {"setup.bat", "setup.sh", "run.bat", "run.sh"} | |
| backend_prefix = "backend/" if project_kind["isFullStack"] else "" | |
| frontend_prefix = "frontend/" if project_kind["isFullStack"] else "" | |
| if project_kind["hasBackend"]: | |
| backend = str(selected_stack.get("backend") or "FastAPI") | |
| if backend in {"FastAPI", "Flask"}: | |
| paths.update( | |
| { | |
| f"{backend_prefix}requirements.txt", | |
| f"{backend_prefix}app/main.py", | |
| f"{backend_prefix}app/routers/health.py", | |
| f"{backend_prefix}app/schemas/health.py", | |
| f"{backend_prefix}app/config.py", | |
| f"{backend_prefix}app/database.py", | |
| } | |
| ) | |
| elif backend in {"Express", "NestJS"}: | |
| paths.update( | |
| { | |
| f"{backend_prefix}package.json", | |
| f"{backend_prefix}server.js", | |
| f"{backend_prefix}src/routes/index.js", | |
| f"{backend_prefix}src/controllers/appController.js", | |
| } | |
| ) | |
| elif backend == "Spring Boot": | |
| paths.update( | |
| { | |
| f"{backend_prefix}pom.xml", | |
| f"{backend_prefix}src/main/java/com/example/demo/Application.java", | |
| f"{backend_prefix}src/main/java/com/example/demo/controller/AppController.java", | |
| f"{backend_prefix}src/main/resources/application.properties", | |
| } | |
| ) | |
| if project_kind["hasFrontend"]: | |
| frontend = str(selected_stack.get("frontend") or "React") | |
| if frontend in {"React", "Next.js", "Vue"}: | |
| paths.update( | |
| { | |
| f"{frontend_prefix}package.json", | |
| f"{frontend_prefix}index.html", | |
| f"{frontend_prefix}vite.config.js", | |
| f"{frontend_prefix}src/main.jsx", | |
| f"{frontend_prefix}src/App.jsx", | |
| f"{frontend_prefix}src/pages/HomePage.jsx", | |
| } | |
| ) | |
| else: | |
| paths.update( | |
| { | |
| f"{frontend_prefix}package.json", | |
| f"{frontend_prefix}index.html", | |
| f"{frontend_prefix}vite.config.js", | |
| f"{frontend_prefix}src/main.js", | |
| f"{frontend_prefix}src/views/home.js", | |
| } | |
| ) | |
| return paths | |
| def _required_runtime_paths( | |
| selected_stack: Mapping[str, Any], | |
| project_kind: Mapping[str, Any], | |
| ) -> set[str]: | |
| return set(_protected_runtime_paths(selected_stack, project_kind)) | |
| def _package_json_paths( | |
| selected_stack: Mapping[str, Any], | |
| project_kind: Mapping[str, Any], | |
| ) -> list[str]: | |
| paths: list[str] = [] | |
| backend_prefix = "backend/" if project_kind["isFullStack"] else "" | |
| frontend_prefix = "frontend/" if project_kind["isFullStack"] else "" | |
| if project_kind["hasBackend"] and str(selected_stack.get("backend") or "") in {"Express", "NestJS"}: | |
| paths.append(f"{backend_prefix}package.json") | |
| if project_kind["hasFrontend"]: | |
| paths.append(f"{frontend_prefix}package.json") | |
| return paths | |
| def _expected_package_scripts( | |
| package_json_path: str, | |
| selected_stack: Mapping[str, Any], | |
| project_kind: Mapping[str, Any], | |
| ) -> set[str]: | |
| if package_json_path.endswith("backend/package.json") or ( | |
| not project_kind["isFullStack"] and project_kind["hasBackend"] | |
| ): | |
| backend = str(selected_stack.get("backend") or "") | |
| if backend in {"Express", "NestJS"}: | |
| return {"start"} | |
| return {"dev"} | |
| def _entry_validation_paths( | |
| selected_stack: Mapping[str, Any], | |
| project_kind: Mapping[str, Any], | |
| ) -> list[str]: | |
| paths: list[str] = [] | |
| backend_prefix = "backend/" if project_kind["isFullStack"] else "" | |
| frontend_prefix = "frontend/" if project_kind["isFullStack"] else "" | |
| if project_kind["hasBackend"]: | |
| backend = str(selected_stack.get("backend") or "FastAPI") | |
| if backend in {"FastAPI", "Flask"}: | |
| paths.append(f"{backend_prefix}app/main.py") | |
| elif backend in {"Express", "NestJS"}: | |
| paths.append(f"{backend_prefix}server.js") | |
| elif backend == "Spring Boot": | |
| paths.append(f"{backend_prefix}src/main/java/com/example/demo/Application.java") | |
| if project_kind["hasFrontend"]: | |
| frontend = str(selected_stack.get("frontend") or "React") | |
| if frontend in {"React", "Next.js", "Vue"}: | |
| paths.extend([f"{frontend_prefix}src/main.jsx", f"{frontend_prefix}src/App.jsx"]) | |
| else: | |
| paths.extend([f"{frontend_prefix}src/main.js", f"{frontend_prefix}index.html"]) | |
| return paths | |
| def _backend_endpoint_paths( | |
| selected_stack: Mapping[str, Any], | |
| project_kind: Mapping[str, Any], | |
| ) -> list[str]: | |
| if not project_kind["hasBackend"]: | |
| return [] | |
| backend_prefix = "backend/" if project_kind["isFullStack"] else "" | |
| backend = str(selected_stack.get("backend") or "FastAPI") | |
| if backend in {"FastAPI", "Flask"}: | |
| return [f"{backend_prefix}app/main.py", f"{backend_prefix}app/routers/health.py"] | |
| if backend in {"Express", "NestJS"}: | |
| return [f"{backend_prefix}server.js", f"{backend_prefix}src/controllers/appController.js"] | |
| if backend == "Spring Boot": | |
| return [f"{backend_prefix}src/main/java/com/example/demo/controller/AppController.java"] | |
| return [] | |
| def _frontend_page_paths( | |
| selected_stack: Mapping[str, Any], | |
| project_kind: Mapping[str, Any], | |
| ) -> list[str]: | |
| if not project_kind["hasFrontend"]: | |
| return [] | |
| frontend_prefix = "frontend/" if project_kind["isFullStack"] else "" | |
| frontend = str(selected_stack.get("frontend") or "React") | |
| if frontend in {"React", "Next.js", "Vue"}: | |
| return [f"{frontend_prefix}src/App.jsx", f"{frontend_prefix}src/pages/HomePage.jsx"] | |
| return [f"{frontend_prefix}index.html", f"{frontend_prefix}src/views/home.js"] | |
| def _valid_package_json(content: str, required_scripts: set[str]) -> bool: | |
| try: | |
| payload = json.loads(content) | |
| except json.JSONDecodeError: | |
| return False | |
| if not isinstance(payload, Mapping): | |
| return False | |
| scripts = payload.get("scripts") | |
| if not isinstance(scripts, Mapping): | |
| return False | |
| return required_scripts.issubset({str(key) for key in scripts.keys()}) | |
| def _valid_entry_file(path: str, content: str) -> bool: | |
| text = content.strip() | |
| if not text: | |
| return False | |
| lower = path.lower() | |
| if lower.endswith("app/main.py"): | |
| return "FastAPI" in text and "app = FastAPI" in text and '@app.get("/")' in text | |
| if lower.endswith("server.js"): | |
| return "express" in text and 'app.use("/", indexRouter)' in text and "app.listen" in text | |
| if lower.endswith("src/main.jsx"): | |
| return "ReactDOM.createRoot" in text and 'from "./App"' in text | |
| if lower.endswith("src/app.jsx"): | |
| return "export default function App" in text | |
| if lower.endswith("src/main.js"): | |
| return "renderHomePage" in text and 'querySelector("#app")' in text | |
| if lower.endswith("index.html"): | |
| return '<div id="root"></div>' in text or '<div id="app"></div>' in text | |
| if lower.endswith("application.java"): | |
| return "@SpringBootApplication" in text and "SpringApplication.run" in text | |
| return True | |
| def _valid_backend_endpoint_file(path: str, content: str) -> bool: | |
| lower = path.lower() | |
| if lower.endswith("app/main.py"): | |
| return '"status": "ok"' in content and '"message": "Project is running"' in content | |
| if lower.endswith("routers/health.py"): | |
| return 'status="ok"' in content and 'message="Project is running"' in content | |
| if lower.endswith("server.js"): | |
| return 'app.use("/", indexRouter)' in content and "app.listen" in content | |
| if lower.endswith("appcontroller.js"): | |
| return 'status: "ok"' in content and 'message: "Project is running"' in content | |
| if lower.endswith("appcontroller.java"): | |
| return '"status", "ok"' in content and '"message", "Project is running"' in content | |
| return True | |
| def _python_compiles(content: str) -> bool: | |
| try: | |
| compile(content, "<generated>", "exec") | |
| except SyntaxError: | |
| return False | |
| return True | |
| def _build_safe_fallback_content(path: str, project_name: str) -> str: | |
| lower = path.lower() | |
| if lower.endswith(".py"): | |
| return f"""def generated_safe_summary() -> dict[str, str]: | |
| return {{"status": "ok", "message": "{project_name} safe fallback is loaded."}} | |
| """ | |
| if lower.endswith(".jsx"): | |
| return f"""export default function SafeFallback() {{ | |
| return <section className="card"><h2>{project_name}</h2><p>Safe fallback UI loaded.</p></section>; | |
| }} | |
| """ | |
| if lower.endswith(".js"): | |
| return f"""export const safeFallback = {{ | |
| status: "ok", | |
| message: "{project_name} safe fallback loaded" | |
| }}; | |
| """ | |
| if lower.endswith(".java"): | |
| return """package com.example.demo.service; | |
| public class SafeFallback { | |
| public String summary() { | |
| return "Safe fallback loaded"; | |
| } | |
| } | |
| """ | |
| return f"# Safe fallback\n\n{project_name} restored this file to preserve a runnable starter.\n" | |
| def _build_java_scripts(target_dir: str) -> dict[str, str]: | |
| directory_prefix = "" if target_dir in {"", "."} else f"{target_dir}/" | |
| windows_prefix = "" if target_dir in {"", "."} else f"{target_dir}\\" | |
| return { | |
| "setup.bat": f"""@echo off | |
| setlocal | |
| where mvn >nul 2>nul && ( | |
| pushd {windows_prefix or "."} | |
| call mvn install | |
| popd | |
| ) || ( | |
| echo Maven not found. Skipping install. | |
| ) | |
| """, | |
| "setup.sh": f"""#!/usr/bin/env bash | |
| set -e | |
| if command -v mvn >/dev/null 2>&1; then | |
| (cd {directory_prefix or "."} && mvn install) | |
| else | |
| echo "Maven not found. Skipping install." | |
| fi | |
| """, | |
| "run.bat": f"""@echo off | |
| setlocal | |
| pushd {windows_prefix or "."} | |
| call mvn spring-boot:run | |
| popd | |
| """, | |
| "run.sh": f"""#!/usr/bin/env bash | |
| set -e | |
| (cd {directory_prefix or "."} && mvn spring-boot:run) | |
| """, | |
| } | |
| def _normalize_preview_files(value: Any) -> list[dict[str, str]]: | |
| if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)): | |
| return [] | |
| files: list[dict[str, str]] = [] | |
| for item in value: | |
| if not isinstance(item, Mapping): | |
| continue | |
| path = _clean_relative_path(item.get("path")) | |
| if not path: | |
| continue | |
| files.append( | |
| { | |
| "path": path, | |
| "content": _trim_content_lines(str(item.get("content") or ""), allow_long=True), | |
| } | |
| ) | |
| return files | |
| def _merge_file_entries( | |
| primary: Sequence[Mapping[str, str]], | |
| secondary: Sequence[Mapping[str, str]], | |
| ) -> list[dict[str, str]]: | |
| merged: dict[str, dict[str, str]] = {} | |
| for file_entry in primary: | |
| merged[str(file_entry["path"])] = { | |
| "path": str(file_entry["path"]), | |
| "content": str(file_entry["content"]), | |
| } | |
| for file_entry in secondary: | |
| merged[str(file_entry["path"])] = { | |
| "path": str(file_entry["path"]), | |
| "content": str(file_entry["content"]), | |
| } | |
| return list(merged.values()) | |
| def _clean_relative_path(value: Any) -> str: | |
| path = str(value or "").replace("\\", "/").strip().strip("/") | |
| if not path or path.startswith(".") or ".." in path.split("/"): | |
| return "" | |
| return path | |
| def _trim_content_lines(content: str, allow_long: bool = False) -> str: | |
| if allow_long: | |
| return content | |
| lines = content.splitlines() | |
| if len(lines) <= MAX_CUSTOM_FILE_LINES: | |
| return content | |
| return "\n".join(lines[:MAX_CUSTOM_FILE_LINES]).rstrip() + "\n" | |
| def _safe_component_name(value: str) -> str: | |
| parts = re.findall(r"[A-Za-z0-9]+", value) | |
| return "".join(part.capitalize() for part in parts) or "GeneratedComponent" | |
| def _safe_python_name(value: str) -> str: | |
| cleaned = re.sub(r"[^a-zA-Z0-9_]+", "_", value).strip("_").lower() | |
| return cleaned or "generated_item" | |
| def _safe_js_name(value: str) -> str: | |
| parts = re.findall(r"[A-Za-z0-9]+", value) | |
| if not parts: | |
| return "generatedItem" | |
| head = parts[0].lower() | |
| tail = "".join(part.capitalize() for part in parts[1:]) | |
| return head + tail | |
| def _prefixed(prefix: str, path: str) -> str: | |
| base = Path(prefix) if prefix else Path() | |
| return (base / path).as_posix() | |