import json import os import logging import mimetypes from datetime import datetime from typing import Optional from contextlib import asynccontextmanager from fastapi import HTTPException, Query from fastapi.responses import Response from backend.auth import get_auth_token from backend.config.Apps import SubApp from backend.apps.outputs.models import ( Output, OutputCreate, OutputUpdate, OutputExecute, OutputExecuteResult, VibeCodeRequest, WorkspaceSeedRequest, PublishPreflightRequest, PublishRequest, PublishPreflightResponse, PublishResult, PublishReview, ) from backend.apps.outputs.executor import execute_backend_code, get_code_warnings from backend.apps.outputs.publish_common import slugify, PublishError from backend.apps.outputs.publish_scan import scan_for_publish, quick_ast_gate from backend.apps.outputs.publish_build import build_static, collect_bundle from backend.apps.outputs.publish_cloud import upload_to_cloud, unpublish_from_cloud from backend.apps.outputs.view_builder_templates import ( VIEW_TEMPLATE_FILES, load_app_builder_skill, seed_webapp_template_workspace, ) from backend.apps.settings.settings import load_settings from backend.config.paths import OUTPUTS_DIR as DATA_DIR, OUTPUTS_WORKSPACE_DIR as WORKSPACE_DIR from backend.apps.outputs.html_inject import ( MODEL_MAP, resolve_model, get_anthropic_client, validate_against_schema, build_data_injection, inject_data_into_html, backend_url_for_workspace, inject_token_into_relative_urls, decode_data_param, ) from backend.apps.outputs.workspace_io import ( load_all, save, load, load_output, walk_directory, ) from backend.apps.outputs.prompts import VIBE_CODE_SYSTEM_PROMPT logger = logging.getLogger(__name__) @asynccontextmanager async def outputs_lifespan(): os.makedirs(DATA_DIR, exist_ok=True) os.makedirs(WORKSPACE_DIR, exist_ok=True) try: yield finally: # Reap every per-app subprocess. Without this each `bash run.sh` (and its vite/uvicorn descendants) reparents to PID 1 when the main backend dies, leaving ghost listeners on the .env-pinned ports that block the next OpenSwarm launch's reload preview. try: from backend.apps.outputs.runtime import manager as runtime_manager killed = await runtime_manager.stop_all() if killed: logger.info("outputs lifespan: reaped %d workspace runtimes on shutdown", killed) except Exception: logger.exception("outputs lifespan: stop_all failed") outputs = SubApp("outputs", outputs_lifespan) # --------------------------------------------------------------------------- File-serving endpoints (for iframe preview with multi-file support) --------------------------------------------------------------------------- @outputs.router.get("/workspace/{workspace_id}/serve/{filepath:path}") async def serve_workspace_file(workspace_id: str, filepath: str, p_d: str = ""): """Serve a file from a workspace folder. For index.html, inject OUTPUT data.""" folder = os.path.join(WORKSPACE_DIR, workspace_id) full_path = os.path.normpath(os.path.join(folder, filepath)) if not full_path.startswith(os.path.normpath(folder)): raise HTTPException(status_code=403, detail="Path traversal not allowed") if not os.path.isfile(full_path): raise HTTPException(status_code=404, detail="File not found") with open(full_path) as f: content = f.read() if filepath == "index.html": input_json, result_json = decode_data_param(p_d) if p_d else ("{}", "null") backend_url_json = backend_url_for_workspace(workspace_id) content = inject_data_into_html(content, input_json, result_json, backend_url_json, with_runtime=True) # Iframe sub-resource fetches (,