Files
Keep/.cline/skills/bmad-agent-builder/scripts/process-template.py

191 lines
5.7 KiB
Python

#!/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 '<stdout>',
'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())