0){
+ topBlocks.push({header:curHeader,body:accum.join('\n')});
+ inBlock=false; accum=[];
+ }
+ } else {
+ // Comment, blank, or stray line between blocks — save as preamble for next block
+ preamble.push(line);
+ }
+ } else {
+ accum.push(line);
+ bDepth+=open-close;
+ if(bDepth<=0){
+ bDepth=0;
+ topBlocks.push({header:curHeader,body:accum.join('\n')});
+ inBlock=false; accum=[]; curHeader='';
+ }
+ }
+ }
+ // Flush remaining
+ if(accum.length) topBlocks.push({header:curHeader||'',body:accum.join('\n')});
+
+ if(topBlocks.length<3) return null;
+
+ // Categorize each block by its header
+ const cats={variables:[],outputs:[],networking:[],compute:[],database:[],
+ firewall:[],loadbalancer:[],storage:[],iam:[],drg:[],data:[],providers:[],other:[]};
+ for(const b of topBlocks){
+ const h=b.header.trim();
+ if(h.startsWith('variable ')) cats.variables.push(b.body);
+ else if(h.startsWith('output ')) cats.outputs.push(b.body);
+ else if(h.startsWith('provider ')) cats.providers.push(b.body);
+ else if(/resource\s+"oci_core_(drg|remote_peering|drg_route|drg_attachment)/.test(h)) cats.drg.push(b.body);
+ else if(/resource\s+"oci_core_(vcn|subnet|internet_gateway|nat_gateway|service_gateway|route_table|security_list|network_security_group|dhcp_options|local_peering_gateway|virtual_circuit)/.test(h)) cats.networking.push(b.body);
+ else if(/resource\s+"oci_core_instance/.test(h)) cats.compute.push(b.body);
+ else if(/resource\s+"oci_(database|nosql|mysql|psql)/.test(h) || /resource\s+"oci_core_autonomous/.test(h)) cats.database.push(b.body);
+ else if(/resource\s+"oci_network_firewall/.test(h)) cats.firewall.push(b.body);
+ else if(/resource\s+"oci_(load_balancer|network_load_balancer)/.test(h)) cats.loadbalancer.push(b.body);
+ else if(/resource\s+"oci_(objectstorage|file_storage)/.test(h)) cats.storage.push(b.body);
+ else if(/resource\s+"oci_identity/.test(h)) cats.iam.push(b.body);
+ else if(h.startsWith('data ')) cats.data.push(b.body);
+ else cats.other.push(b.body);
+ }
+
+ // Build result files in logical order
+ const result=[];
+ const add=(name,arr)=>{if(arr.length) result.push({name,content:arr.join('\n\n')})};
+ add('variables.tf', cats.variables);
+ add('providers.tf', cats.providers);
+ add('networking.tf', cats.networking);
+ add('drg.tf', cats.drg);
+ add('compute.tf', cats.compute);
+ add('database.tf', cats.database);
+ add('firewall.tf', cats.firewall);
+ add('loadbalancer.tf', cats.loadbalancer);
+ add('storage.tf', cats.storage);
+ add('iam.tf', cats.iam);
+ add('data.tf', cats.data);
+ add('main.tf', cats.other);
+ add('outputs.tf', cats.outputs);
+
+ return result.length>=3 ? result : null;
+}
+
function fmTf(t){
// Extract ALL hcl blocks, parse // filename: comments, build separate files
const codeBlocks=[];
let r=t.replace(/```(?:hcl|terraform)\s*\n([\s\S]*?)```/g,(m,code)=>{
- const trimmed=code.trim();
+ const trimmed=_fixSingleLineBlocks(code.trim());
// Check for // filename: comment on first line
const fnMatch=trimmed.match(/^\/\/\s*filename:\s*(\S+)/);
const fileName=fnMatch?fnMatch[1]:'main.tf';
@@ -736,30 +850,52 @@ function fmTf(t){
return ''+
S.tfPlan.map(x=>'
+'+escHtml(x.type)+''+escHtml(x.desc)+'
').join('')+'
';
});
- // Build files list — merge new blocks with existing files (corrections preserve untouched files)
+ // Auto-split: if model returned 1-2 large blocks, split into multiple files
+ // Runs on BOTH first generation and corrections — monolithic responses always get split
+ let didAutoSplit=false;
+ if(codeBlocks.length<=2){
+ const totalContent=codeBlocks.map(b=>b.content).join('\n\n');
+ if(totalContent.length>2000){
+ const split=_splitTfMonolith(totalContent);
+ if(split){
+ codeBlocks.length=0;
+ split.forEach(f=>codeBlocks.push(f));
+ didAutoSplit=true;
+ // Rebuild HTML with split files
+ const splitHtml=split.map((f,i)=>
+ ''+
+ '
'+escHtml(f.content)+'
'
+ ).join('');
+ // Replace the original single code block HTML
+ r=r.replace(/[\s\S]*?<\/pre><\/div>/g,'');
+ r=splitHtml+r;
+ console.log('[TF] Auto-split monolithic file into '+split.length+' files');
+ }
+ }
+ }
+ // Build files list
if(codeBlocks.length){
const newMap={};
codeBlocks.forEach(b=>{
if(newMap[b.name])newMap[b.name]+='\n\n'+b.content;
else newMap[b.name]=b.content;
});
- // If existing files: merge (update matched, keep unmatched)
- if(S.tfFiles.length){
+ // If auto-split happened, REPLACE all files (monolithic = full regeneration, not partial correction)
+ // If model returned multiple named files (no auto-split), use merge logic for corrections
+ if(didAutoSplit || !S.tfFiles.length){
+ S.tfFiles=Object.entries(newMap).map(([name,content])=>({name,content,size:content.length,type:'hcl'}));
+ }else{
const merged=[];
const usedNew=new Set();
- // Update existing files with new content if name matches
for(const f of S.tfFiles){
if(newMap[f.name]!=null){merged.push({name:f.name,content:newMap[f.name],size:newMap[f.name].length,type:'hcl'});usedNew.add(f.name)}
else{merged.push(f)}
}
- // Add any truly new files
for(const[name,content] of Object.entries(newMap)){
if(!usedNew.has(name))merged.push({name,content,size:content.length,type:'hcl'})
}
S.tfFiles=merged;
- }else{
- // First generation: set all
- S.tfFiles=Object.entries(newMap).map(([name,content])=>({name,content,size:content.length,type:'hcl'}));
}
S.tfCode=S.tfFiles.map(f=>'// filename: '+f.name+'\n'+f.content).join('\n\n');
}