ResolversHow-To Guides
Merge Text Files
Handle text file conflicts from layered templates
Merge Text Files
Handle text-based file conflicts by combining content from multiple template layers.
Understanding Text Merging
Text files (.gitignore, Dockerfile, scripts) often need to be merged when templates are layered:
- Line-based: Add unique lines from each layer
- Section-based: Merge by named sections
- Append: Add content to end of file
Line-Based Merge
Combine lines from all layers, removing duplicates.
index.ts
import { ResolverOutput, StartResolverWithLambda } from '@atomicloud/cyan-sdk';StartResolverWithLambda(async (input): Promise<ResolverOutput> => {// Collect all lines from all filesconst allLines: string[] = [];// Sort by layer (lower layer = higher priority)const sorted = [...input.files].sort((a, b) => a.origin.layer - b.origin.layer);for (const file of sorted) {const lines = file.content.split('\n');allLines.push(...lines);}// Remove duplicates while preserving orderconst seen = new Set<string>();const uniqueLines: string[] = [];for (const line of allLines) {const trimmed = line.trim();// Skip empty lines and comments in dedup checkconst key = trimmed === '' || trimmed.startsWith('#') ? line : trimmed;if (!seen.has(key)) {seen.add(key);uniqueLines.push(line);}}return {path: input.files[0].path,content: uniqueLines.join('\n')};});
main.py
from cyanprintsdk.domain.resolver.input import ResolverInputfrom cyanprintsdk.domain.resolver.output import ResolverOutputfrom cyanprintsdk.main import start_resolver_with_fnasync def resolver(input: ResolverInput) -> ResolverOutput:# Sort by layer (lower layer = higher priority)sorted_files = sorted(input.files, key=lambda f: f.origin.layer)# Collect all lines from all filesall_lines: list[str] = []for file in sorted_files:lines = file.content.split('\n')all_lines.extend(lines)# Remove duplicates while preserving orderseen = set()unique_lines: list[str] = []for line in all_lines:trimmed = line.strip()# Skip empty lines and comments in dedup checkkey = line if (not trimmed or trimmed.startswith('#')) else trimmedif key not in seen:seen.add(key)unique_lines.append(line)return ResolverOutput(path=input.files[0].path,content='\n'.join(unique_lines))if __name__ == "__main__":start_resolver_with_fn(resolver)
Program.cs
using sulfone_helium;using sulfone_helium.Domain.Resolver;CyanEngine.StartResolver(args,async (ResolverInput input) =>{// Sort by layer (lower layer = higher priority)var sorted = input.Files.OrderBy(f => f.Origin.Layer).ToList();// Collect all lines from all filesvar allLines = new List<string>();foreach (var file in sorted){allLines.AddRange(file.Content.Split('\n'));}// Remove duplicates while preserving ordervar seen = new HashSet<string>();var uniqueLines = new List<string>();foreach (var line in allLines){var trimmed = line.Trim();// Skip empty lines and comments in dedup checkvar key = string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("#")? line : trimmed;if (seen.Add(key)){uniqueLines.Add(line);}}return new ResolverOutput(input.Files.First().Path,string.Join("\n", uniqueLines));});
Section-Based Merge
Merge files by named sections using markers.
index.ts
import { ResolverOutput, StartResolverWithLambda } from '@atomicloud/cyan-sdk';interface SectionedFile {[section: string]: string[];}function parseSections(content: string): SectionedFile {const sections: SectionedFile = { '_header_': [] };let currentSection = '_header_';for (const line of content.split('\n')) {const match = line.match(/^##\s*([\w-]+)\s*##/);if (match) {currentSection = match[1];sections[currentSection] = [];} else {if (!sections[currentSection]) {sections[currentSection] = [];}sections[currentSection].push(line);}}return sections;}function mergeSections(target: SectionedFile, source: SectionedFile): SectionedFile {const result = { ...target };for (const [section, lines] of Object.entries(source)) {if (result[section]) {// Merge lines, removing duplicatesconst combined = [...result[section], ...lines];const seen = new Set<string>();result[section] = combined.filter(line => {const key = line.trim();if (seen.has(key)) return false;seen.add(key);return true;});} else {result[section] = [...lines];}}return result;}StartResolverWithLambda(async (input): Promise<ResolverOutput> => {const sorted = [...input.files].sort((a, b) => a.origin.layer - b.origin.layer);let merged: SectionedFile = {};for (const file of sorted) {const sections = parseSections(file.content);merged = mergeSections(merged, sections);}// Reconstruct file contentconst output: string[] = [];// Add header firstif (merged['_header_']?.length) {output.push(...merged['_header_']);delete merged['_header_'];}// Add other sectionsfor (const [section, lines] of Object.entries(merged)) {output.push(`## ${section} ##`);output.push(...lines);}return {path: input.files[0].path,content: output.join('\n')};});
main.py
from typing import Dict, Listfrom cyanprintsdk.domain.resolver.input import ResolverInputfrom cyanprintsdk.domain.resolver.output import ResolverOutputfrom cyanprintsdk.main import start_resolver_with_fndef parse_sections(content: str) -> Dict[str, List[str]]:sections: Dict[str, List[str]] = {"_header_": []}current_section = "_header_"for line in content.split('\n'):import rematch = re.match(r'^##\s*([\w-]+)\s*##', line)if match:current_section = match.group(1)sections[current_section] = []else:if current_section not in sections:sections[current_section] = []sections[current_section].append(line)return sectionsdef merge_sections(target: Dict[str, List[str]], source: Dict[str, List[str]]) -> Dict[str, List[str]]:result = dict(target)for section, lines in source.items():if section in result:# Merge lines, removing duplicatescombined = result[section] + linesseen = set()result[section] = [line for line in combinedif not (line.strip() in seen or seen.add(line.strip()))] # type: ignoreelse:result[section] = list(lines)return resultasync def resolver(input: ResolverInput) -> ResolverOutput:sorted_files = sorted(input.files, key=lambda f: f.origin.layer)merged: Dict[str, List[str]] = {}for file in sorted_files:sections = parse_sections(file.content)merged = merge_sections(merged, sections)# Reconstruct file contentoutput: List[str] = []# Add header firstif merged.get("_header_"):output.extend(merged["_header_"])del merged["_header_"]# Add other sectionsfor section, lines in merged.items():output.append(f"## {section} ##")output.extend(lines)return ResolverOutput(path=input.files[0].path,content='\n'.join(output))if __name__ == "__main__":start_resolver_with_fn(resolver)
Program.cs
using System.Text.RegularExpressions;using sulfone_helium;using sulfone_helium.Domain.Resolver;public record SectionedFile(Dictionary<string, List<string>> Sections);public class TextMerger{public static Dictionary<string, List<string>> ParseSections(string content){var sections = new Dictionary<string, List<string>> { ["_header_"] = new List<string>() };var currentSection = "_header_";foreach (var line in content.Split('\n')){var match = Regex.Match(line, @"^##\s*([\w-]+)\s*##");if (match.Success){currentSection = match.Groups[1].Value;sections[currentSection] = new List<string>();}else{if (!sections.ContainsKey(currentSection))sections[currentSection] = new List<string>();sections[currentSection].Add(line);}}return sections;}public static Dictionary<string, List<string>> MergeSections(Dictionary<string, List<string>> target,Dictionary<string, List<string>> source){var result = new Dictionary<string, List<string>>(target);foreach (var (section, lines) in source){if (result.TryGetValue(section, out var existingLines)){var combined = existingLines.Concat(lines);var seen = new HashSet<string>();result[section] = combined.Where(line => seen.Add(line.Trim())).ToList();}else{result[section] = new List<string>(lines);}}return result;}}CyanEngine.StartResolver(args,async (ResolverInput input) =>{var sorted = input.Files.OrderBy(f => f.Origin.Layer).ToList();var merged = new Dictionary<string, List<string>>();foreach (var file in sorted){var sections = TextMerger.ParseSections(file.Content);merged = TextMerger.MergeSections(merged, sections);}// Reconstruct file contentvar output = new List<string>();// Add header firstif (merged.TryGetValue("_header_", out var headerLines)){output.AddRange(headerLines);merged.Remove("_header_");}// Add other sectionsforeach (var (section, lines) in merged){output.Add($"## {section} ##");output.AddRange(lines);}return new ResolverOutput(input.Files.First().Path,string.Join("\n", output));});
Section markers use the format ## section-name ##. Lines before the first marker go into a special _header_ section.
Append Strategy
Simply append content from each layer.
index.ts
import { ResolverOutput, StartResolverWithLambda } from '@atomicloud/cyan-sdk';StartResolverWithLambda(async (input): Promise<ResolverOutput> => {const sorted = [...input.files].sort((a, b) => a.origin.layer - b.origin.layer);const parts: string[] = [];for (const file of sorted) {parts.push(file.content);}return {path: input.files[0].path,content: parts.join('\n\n# --- Layer Separator ---\n\n')};});
main.py
from cyanprintsdk.domain.resolver.input import ResolverInputfrom cyanprintsdk.domain.resolver.output import ResolverOutputfrom cyanprintsdk.main import start_resolver_with_fnasync def resolver(input: ResolverInput) -> ResolverOutput:sorted_files = sorted(input.files, key=lambda f: f.origin.layer)parts = [file.content for file in sorted_files]return ResolverOutput(path=input.files[0].path,content='\n\n# --- Layer Separator ---\n\n'.join(parts))if __name__ == "__main__":start_resolver_with_fn(resolver)
Program.cs
using sulfone_helium;using sulfone_helium.Domain.Resolver;CyanEngine.StartResolver(args,async (ResolverInput input) =>{var sorted = input.Files.OrderBy(f => f.Origin.Layer).ToList();var parts = sorted.Select(f => f.Content);return new ResolverOutput(input.Files.First().Path,string.Join("\n\n# --- Layer Separator ---\n\n", parts));});
Example: .gitignore Resolver
A common use case is merging .gitignore files:
index.ts
import { ResolverOutput, StartResolverWithLambda } from '@atomicloud/cyan-sdk';StartResolverWithLambda(async (input): Promise<ResolverOutput> => {// Collect all patternsconst patterns: string[] = [];const sorted = [...input.files].sort((a, b) => a.origin.layer - b.origin.layer);for (const file of sorted) {const lines = file.content.split('\n');for (const line of lines) {const trimmed = line.trim();// Skip empty lines and commentsif (trimmed && !trimmed.startsWith('#')) {patterns.push(trimmed);}}}// Deduplicate while preserving orderconst seen = new Set<string>();const uniquePatterns: string[] = [];for (const pattern of patterns) {if (!seen.has(pattern)) {seen.add(pattern);uniquePatterns.push(pattern);}}// Group by category (optional enhancement)const output = `# Auto-generated .gitignore\n\n${uniquePatterns.join('\n')}\n`;return {path: input.files[0].path,content: output};});
main.py
from cyanprintsdk.domain.resolver.input import ResolverInputfrom cyanprintsdk.domain.resolver.output import ResolverOutputfrom cyanprintsdk.main import start_resolver_with_fnasync def resolver(input: ResolverInput) -> ResolverOutput:# Collect all patternspatterns: list[str] = []sorted_files = sorted(input.files, key=lambda f: f.origin.layer)for file in sorted_files:for line in file.content.split('\n'):trimmed = line.strip()# Skip empty lines and commentsif trimmed and not trimmed.startswith('#'):patterns.append(trimmed)# Deduplicate while preserving orderseen = set()unique_patterns: list[str] = []for pattern in patterns:if pattern not in seen:seen.add(pattern)unique_patterns.append(pattern)# Group by category (optional enhancement)output = f"# Auto-generated .gitignore\n\n{chr(10).join(unique_patterns)}\n"return ResolverOutput(path=input.files[0].path,content=output)if __name__ == "__main__":start_resolver_with_fn(resolver)
Program.cs
using sulfone_helium;using sulfone_helium.Domain.Resolver;CyanEngine.StartResolver(args,async (ResolverInput input) =>{// Collect all patternsvar patterns = new List<string>();var sorted = input.Files.OrderBy(f => f.Origin.Layer).ToList();foreach (var file in sorted){foreach (var line in file.Content.Split('\n')){var trimmed = line.Trim();// Skip empty lines and commentsif (!string.IsNullOrEmpty(trimmed) && !trimmed.StartsWith("#")){patterns.Add(trimmed);}}}// Deduplicate while preserving ordervar seen = new HashSet<string>();var uniquePatterns = new List<string>();foreach (var pattern in patterns){if (seen.Add(pattern)){uniquePatterns.Add(pattern);}}// Group by category (optional enhancement)var output = $"# Auto-generated .gitignore\n\n{string.Join("\n", uniquePatterns)}\n";return new ResolverOutput(input.Files.First().Path,output);});