#!/usr/bin/env python3 """Process BMad agent template files. Performs deterministic variable substitution and conditional block processing on template files from assets/. Replaces {varName} placeholders with provided values and evaluates {if-X}...{/if-X} conditional blocks, keeping content when the condition is in the --true list and removing the entire block otherwise. """ # /// script # requires-python = ">=3.9" # /// from __future__ import annotations import argparse import json import re import sys def process_conditionals(text: str, true_conditions: set[str]) -> tuple[str, list[str], list[str]]: """Process {if-X}...{/if-X} conditional blocks, innermost first. Returns (processed_text, conditions_true, conditions_false). """ conditions_true: list[str] = [] conditions_false: list[str] = [] # Process innermost blocks first to handle nesting pattern = re.compile( r'\{if-([a-zA-Z0-9_-]+)\}(.*?)\{/if-\1\}', re.DOTALL, ) changed = True while changed: changed = False match = pattern.search(text) if match: changed = True condition = match.group(1) inner = match.group(2) if condition in true_conditions: # Keep the inner content, strip the markers # Remove a leading newline if the opening tag was on its own line replacement = inner if condition not in conditions_true: conditions_true.append(condition) else: # Remove the entire block replacement = '' if condition not in conditions_false: conditions_false.append(condition) text = text[:match.start()] + replacement + text[match.end():] # Clean up blank lines left by removed blocks: collapse 3+ consecutive # newlines down to 2 (one blank line) text = re.sub(r'\n{3,}', '\n\n', text) return text, conditions_true, conditions_false def process_variables(text: str, variables: dict[str, str]) -> tuple[str, list[str]]: """Replace {varName} placeholders with provided values. Only replaces variables that are in the provided mapping. Leaves unmatched {variables} untouched (they may be runtime config). Returns (processed_text, list_of_substituted_var_names). """ substituted: list[str] = [] for name, value in variables.items(): placeholder = '{' + name + '}' if placeholder in text: text = text.replace(placeholder, value) if name not in substituted: substituted.append(name) return text, substituted def parse_var(s: str) -> tuple[str, str]: """Parse a key=value string. Raises argparse error on bad format.""" if '=' not in s: raise argparse.ArgumentTypeError( f"Invalid variable format: '{s}' (expected key=value)" ) key, _, value = s.partition('=') if not key: raise argparse.ArgumentTypeError( f"Invalid variable format: '{s}' (empty key)" ) return key, value def main() -> int: parser = argparse.ArgumentParser( description='Process BMad agent template files with variable substitution and conditional blocks.', ) parser.add_argument( 'template', help='Path to the template file to process', ) parser.add_argument( '-o', '--output', help='Write processed output to file (default: stdout)', ) parser.add_argument( '--var', action='append', default=[], metavar='key=value', help='Variable substitution (repeatable). Example: --var skillName=my-agent', ) parser.add_argument( '--true', action='append', default=[], dest='true_conditions', metavar='CONDITION', help='Condition name to treat as true (repeatable). Example: --true pulse --true evolvable', ) parser.add_argument( '--json', action='store_true', dest='json_output', help='Output processing metadata as JSON to stderr', ) args = parser.parse_args() # Parse variables variables: dict[str, str] = {} for v in args.var: try: key, value = parse_var(v) except argparse.ArgumentTypeError as e: print(f"Error: {e}", file=sys.stderr) return 2 variables[key] = value true_conditions = set(args.true_conditions) # Read template try: with open(args.template, encoding='utf-8') as f: content = f.read() except FileNotFoundError: print(f"Error: Template file not found: {args.template}", file=sys.stderr) return 2 except OSError as e: print(f"Error reading template: {e}", file=sys.stderr) return 1 # Process: conditionals first, then variables content, conds_true, conds_false = process_conditionals(content, true_conditions) content, vars_substituted = process_variables(content, variables) # Write output output_file = args.output try: if output_file: with open(output_file, 'w', encoding='utf-8') as f: f.write(content) else: sys.stdout.write(content) except OSError as e: print(f"Error writing output: {e}", file=sys.stderr) return 1 # JSON metadata to stderr if args.json_output: metadata = { 'processed': True, 'output_file': output_file or '', 'vars_substituted': vars_substituted, 'conditions_true': conds_true, 'conditions_false': conds_false, } print(json.dumps(metadata, indent=2), file=sys.stderr) return 0 if __name__ == '__main__': sys.exit(main())