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 (,