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, AgentCreateAppRequest, PublishPreflightRequest, PublishRequest, PublishPreflightResponse, PublishResult, PublishReview, ) from backend.apps.outputs.code_safety import get_code_warnings from backend.apps.outputs.executor import execute_backend_code from backend.apps.outputs.publish_capability import check_publish_capability 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 ( 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, resolve_in_workspace, walk_directory, workspace_root, would_shrink_oversize_file, ) 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) # One-time sweep: an app whose record vanished still has its work on disk, and no way for the user to know it is there. try: from backend.apps.outputs.recover_orphaned_apps import recover_orphaned_apps recover_orphaned_apps() except Exception: logger.exception("orphaned-app recovery failed; apps stay hidden but nothing else breaks") 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 = resolve_in_workspace(folder, filepath) if full_path is None: 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 (,