1+ from fastapi import APIRouter , HTTPException
2+ from pydantic import BaseModel
3+ from typing import List , Dict , Any , Optional
4+ import json
5+ from app .services .actions_loader import actions_loader
6+
7+ router = APIRouter (prefix = "/api/v2" , tags = ["generate" ])
8+
9+ class GenerateRequest (BaseModel ):
10+ action_ids : List [str ]
11+ formats : List [str ] = ["claude" ] # claude, cursor, agents
12+ source : str = "scratch" # "repo", "template", or "scratch"
13+ repo_url : Optional [str ] = None # For tracking the source repo when source="repo"
14+
15+ class GenerateResponse (BaseModel ):
16+ files : Dict [str , str ]
17+ patch : str
18+ source : str
19+
20+ @router .post ("/generate" , operation_id = "generate_configuration" )
21+ async def generate_configuration (request : GenerateRequest ) -> GenerateResponse :
22+ """Generate configuration files from selected action IDs"""
23+
24+ files = {}
25+
26+ # Load action details
27+ selected_agents = []
28+ selected_rules = []
29+ selected_mcps = []
30+
31+ for action_id in request .action_ids :
32+ # Try to find the action in different categories
33+
34+ # Check agents
35+ agent = actions_loader .get_agent (action_id )
36+ if agent :
37+ selected_agents .append (agent )
38+ continue
39+
40+ # Check rules
41+ rule = actions_loader .get_rule (action_id )
42+ if rule :
43+ selected_rules .append (rule )
44+ continue
45+
46+ # Check MCPs
47+ mcp = actions_loader .get_mcp (action_id )
48+ if mcp :
49+ selected_mcps .append (mcp )
50+ continue
51+
52+ # Generate files based on selected formats
53+ for format_type in request .formats :
54+ if format_type == "claude" :
55+ # Generate CLAUDE.md if there are rules
56+ if selected_rules :
57+ claude_content = ""
58+ for rule in selected_rules :
59+ if rule .get ('content' ):
60+ claude_content += rule ['content' ].strip () + "\n \n "
61+
62+ if claude_content :
63+ files ['CLAUDE.md' ] = claude_content .strip ()
64+
65+ # Generate agent files for Claude format
66+ for agent in selected_agents :
67+ if agent .get ('content' ):
68+ filename = agent .get ('filename' , f"{ agent ['name' ]} .md" )
69+ files [f".claude/agents/{ filename } " ] = agent ['content' ]
70+
71+ elif format_type == "cursor" :
72+ # Generate .cursorrules file
73+ if selected_rules :
74+ cursor_content = ""
75+ for rule in selected_rules :
76+ if rule .get ('content' ):
77+ cursor_content += rule ['content' ].strip () + "\n \n "
78+
79+ if cursor_content :
80+ files ['.cursorrules' ] = cursor_content .strip ()
81+
82+ elif format_type == "agents" :
83+ # Generate AGENTS.md file with rules (copy of CLAUDE.md)
84+ if selected_rules :
85+ agents_content = ""
86+ for rule in selected_rules :
87+ if rule .get ('content' ):
88+ agents_content += rule ['content' ].strip () + "\n \n "
89+
90+ if agents_content :
91+ files ['AGENTS.md' ] = agents_content .strip ()
92+
93+ # Generate .mcp.json if there are MCPs
94+ if selected_mcps :
95+ mcp_config = {"mcpServers" : {}}
96+ for mcp in selected_mcps :
97+ if mcp .get ('config' ):
98+ mcp_config ["mcpServers" ][mcp ['name' ]] = mcp ['config' ]
99+
100+ if mcp_config ["mcpServers" ]:
101+ files ['.mcp.json' ] = json .dumps (mcp_config , indent = 2 )
102+
103+ # Generate patch file
104+ patch = generate_patch (files , request .source , request .repo_url )
105+
106+ return GenerateResponse (files = files , patch = patch , source = request .source )
107+
108+ def generate_patch (files : Dict [str , str ], source : str = "scratch" , repo_url : str = None ) -> str :
109+ """
110+ Generate a unified diff patch from the files.
111+
112+ Args:
113+ files: Dictionary of file paths and their contents
114+ source: Source of the generation ("repo", "template", or "scratch")
115+ repo_url: URL of source repository if source is "repo"
116+
117+ Returns:
118+ Unified diff patch string that can be applied with patch command
119+ """
120+ patch_lines = []
121+
122+ # Add a comment header explaining the patch
123+ if source == "repo" and repo_url :
124+ patch_lines .append (f"# Gitrules configuration patch generated from repository: { repo_url } " )
125+ patch_lines .append ("# Apply with: git apply <this-patch>" )
126+ use_git_format = True
127+ elif source == "template" :
128+ patch_lines .append ("# Gitrules configuration patch generated from template" )
129+ patch_lines .append ("# Apply with: patch -p0 < <this-patch>" )
130+ use_git_format = False
131+ else :
132+ patch_lines .append ("# Gitrules configuration patch generated from scratch" )
133+ patch_lines .append ("# Apply with: patch -p0 < <this-patch>" )
134+ use_git_format = False
135+
136+ patch_lines .append ("" )
137+
138+ for filepath , content in files .items ():
139+ if use_git_format :
140+ # Git format
141+ patch_lines .append (f"diff --git a/{ filepath } b/{ filepath } " )
142+ patch_lines .append ("new file mode 100644" )
143+ patch_lines .append ("index 0000000..1234567" )
144+ patch_lines .append ("--- /dev/null" )
145+ patch_lines .append (f"+++ b/{ filepath } " )
146+ else :
147+ # Standard patch format
148+ patch_lines .append (f"--- /dev/null" )
149+ patch_lines .append (f"+++ { filepath } " )
150+
151+ lines = content .split ('\n ' )
152+ if lines and lines [- 1 ] == '' :
153+ lines .pop () # Remove empty last line if present
154+
155+ patch_lines .append (f"@@ -0,0 +1,{ len (lines )} @@" )
156+
157+ for line in lines :
158+ patch_lines .append (f"+{ line } " )
159+
160+ patch_lines .append ("" ) # Empty line between files
161+
162+ return '\n ' .join (patch_lines )
0 commit comments