MVP-Agent / src /file_manager.py
Furqan Ahmad Rao
feat: implement in-memory ZIP creation and enhance file management functionality
cfda7db
"""
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