""" File Manager Module - Handles saving and managing generated MVP files """ import os import io import shutil import zipfile import tempfile import re from typing import Dict, Optional, Tuple from datetime import datetime from .mcp_http_clients import FileManagerMCPClient def sanitize_markdown(content: str) -> str: """ Sanitize markdown content by removing invisible/control characters. Args: content: Raw markdown content Returns: Sanitized markdown content """ if not content: return content # Remove common invisible characters (zero-width spaces, BOM, etc.) # Keep newlines, tabs, and standard spaces sanitized = content.replace('\ufeff', '') # BOM sanitized = sanitized.replace('\u200b', '') # Zero-width space sanitized = sanitized.replace('\u200c', '') # Zero-width non-joiner sanitized = sanitized.replace('\u200d', '') # Zero-width joiner sanitized = sanitized.replace('\ufffe', '') # Reverse BOM # Remove any other control characters except newline, carriage return, and tab sanitized = ''.join(char for char in sanitized if ord(char) >= 32 or char in '\n\r\t') # Normalize line endings to \n sanitized = sanitized.replace('\r\n', '\n').replace('\r', '\n') return sanitized class FileManager: """Manages saving MVP files - uses in-memory ZIP for HF Spaces compatibility""" def __init__(self, output_dir: str = "outputs"): """ Initialize file manager Args: output_dir: Directory to save files (default: outputs/) - NOT USED in production """ self.output_dir = output_dir self.mcp_client = FileManagerMCPClient() # No longer creating output directory - we use temp files instead def create_zip_in_memory(self, files: Dict[str, str], idea: str) -> str: """ Create a temporary ZIP file from generated content. The ZIP file is stored in system temp directory and will be auto-cleaned. Args: files: Dictionary with file contents (keys: features_md, architecture_md, etc.) idea: The startup idea (used for naming) Returns: Path to temporary ZIP file (will be auto-deleted by OS) """ # Prepare file content zip_content = {} # File mapping file_names = { "overview_md": "overview.md", "features_md": "features.md", "architecture_md": "architecture.md", "design_md": "design.md", "user_flow_md": "user_flow.md", "roadmap_md": "roadmap.md", "business_model_md": "business_model.md", "testing_plan_md": "testing_plan.md" } # Add all markdown files (sanitized) for key, filename in file_names.items(): if key in files: # Sanitize content before writing zip_content[filename] = sanitize_markdown(files[key]) # Add README readme_content = f"""# MVP Blueprint **Idea:** {idea} **Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} **Files:** - overview.md - MVP overview and usage guide - features.md - Feature specifications - architecture.md - Technical architecture - design.md - Design philosophy - user_flow.md - User journey maps - roadmap.md - 6-week launch plan - business_model.md - Business model specification - testing_plan.md - Testing plan and QA --- *Generated by MVP Agent* """ zip_content["README.md"] = readme_content timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") safe_idea = "".join(c for c in idea[:30] if c.isalnum() or c in (" ", "-", "_")).strip() safe_idea = safe_idea.replace(" ", "_") or "mvp" zip_filename = f'mvp_{safe_idea}_{timestamp}.zip' # Try MCP first try: resp = self.mcp_client.create_zip_from_memory(zip_content, zip_filename) if resp.get("success") and resp.get("path"): print(f"✅ Successfully created ZIP via MCP: {resp.get('path')}") return resp.get("path") else: print(f"⚠️ MCP ZIP creation failed: {resp.get('message')}. Falling back to local.") except Exception as e: print(f"⚠️ MCP ZIP creation error: {e}. Falling back to local.") # Fallback: Create temp file locally temp_zip = tempfile.NamedTemporaryFile( mode='w+b', suffix='.zip', prefix=f'mvp_{safe_idea}_{timestamp}_', delete=False # We'll let Gradio handle deletion after download ) temp_zip.close() # Close it so we can write to it with zipfile # Create ZIP in the temp file with atomic write with zipfile.ZipFile(temp_zip.name, 'w', zipfile.ZIP_DEFLATED) as zipf: for filename, content in zip_content.items(): zipf.writestr(filename, content) print(f"✅ Successfully created ZIP locally: {temp_zip.name}") return temp_zip.name def save_mvp_files(self, files: Dict[str, str], idea: str) -> Dict[str, str]: """ Create in-memory ZIP file for download. NO persistent storage - perfect for Hugging Face Spaces. Args: files: Dictionary with file contents (keys: features_md, architecture_md, etc.) idea: The startup idea (used for naming) Returns: Dictionary with 'zip' key pointing to temporary file path """ # Create temporary ZIP file zip_path = self.create_zip_in_memory(files, idea) return { "zip": zip_path, "directory": "temp", # No actual directory } def get_latest_mvp_dir(self) -> str: """ Deprecated - not used with in-memory ZIP approach. Returns empty string. """ return "" # Singleton instance _file_manager = None def get_file_manager() -> FileManager: """Get or create the file manager singleton""" global _file_manager if _file_manager is None: _file_manager = FileManager() return _file_manager