Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 63 additions & 9 deletions struct_module/commands/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,21 +136,35 @@ def execute(self, args):
self.logger.error("Post-hook failed.")
return

def _create_structure(self, args, mappings=None):
def _create_structure(self, args, mappings=None, summary=None, print_summary=True):
if isinstance(args, dict):
args = argparse.Namespace(**args)
this_file = os.path.dirname(os.path.realpath(__file__))
contribs_path = os.path.join(this_file, "..", "contribs")

config = self._load_yaml_config(args.structure_definition, args.structures_path)
if config is None:
return
return summary if summary is not None else None

template_vars = dict(item.split('=') for item in args.vars.split(',')) if args.vars else None
config_structure = config.get('files', config.get('structure', []))
config_folders = config.get('folders', [])
config_variables = config.get('variables', [])

# Action counters for final summary (initialize once and reuse across recursive calls)
if summary is None:
summary = {
"created": 0,
"updated": 0,
"appended": 0,
"skipped": 0,
"backed_up": 0,
"renamed": 0,
"folders": 0,
"dry_run_created": 0,
"dry_run_updated": 0,
}

for item in config_structure:
self.logger.debug(f"Processing item: {item}")
for name, content in item.items():
Expand Down Expand Up @@ -180,7 +194,7 @@ def _create_structure(self, args, mappings=None):
file_path_to_create = os.path.join(args.base_path, name)
existing_content = None
if os.path.exists(file_path_to_create):
self.logger.warning(f"⚠️ File already exists: {file_path_to_create}")
self.logger.info(f"ℹ️ Exists: {file_path_to_create}")
with open(file_path_to_create, 'r') as existing_file:
existing_content = existing_file.read()

Expand Down Expand Up @@ -213,6 +227,10 @@ def _create_structure(self, args, mappings=None):
if existing_content is not None:
action = "update"
print(f"[DRY RUN] {action}: {file_path_to_create}")
if action == "create":
summary["dry_run_created"] += 1
else:
summary["dry_run_updated"] += 1
import difflib
new_content = file_item.content if file_item.content.endswith("\n") else file_item.content + "\n"
old_content = (existing_content if existing_content is not None else "")
Expand All @@ -225,26 +243,39 @@ def _create_structure(self, args, mappings=None):
)
print("".join(diff))
else:
file_item.create(
result = file_item.create(
args.base_path,
args.dry_run or False,
args.backup or None,
args.file_strategy or 'overwrite'
)
if isinstance(result, dict):
if result.get("action") == "created":
summary["created"] += 1
elif result.get("action") == "updated":
summary["updated"] += 1
elif result.get("action") == "appended":
summary["appended"] += 1
elif result.get("action") == "skipped":
summary["skipped"] += 1
if result.get("backed_up_to"):
summary["backed_up"] += 1
if result.get("renamed_from"):
summary["renamed"] += 1

for item in config_folders:
for folder, content in item.items():
folder_path = os.path.join(args.base_path, folder)
if hasattr(args, 'output') and args.output == 'file':
os.makedirs(folder_path, exist_ok=True)
self.logger.info(f"Created folder")
self.logger.info(f" Folder: {folder_path}")
self.logger.info(f"📁 Created folder: {folder_path}")
summary["folders"] += 1

# check if content has struct value
if 'struct' in content:
self.logger.info(f"Generating structure")
self.logger.info(f" Folder: {folder}")
self.logger.info(f" Struct:")
self.logger.info(f" Struct(s):")
if isinstance(content['struct'], list):
# iterate over the list of structures
for struct in content['struct']:
Expand Down Expand Up @@ -279,26 +310,49 @@ def _create_structure(self, args, mappings=None):
'base_path': folder_path,
'structures_path': args.structures_path,
'dry_run': args.dry_run,
'diff': getattr(args, 'diff', False),
'output': getattr(args, 'output', 'file'),
'vars': merged_vars,
'backup': args.backup,
'file_strategy': args.file_strategy,
'global_system_prompt': args.global_system_prompt,
'input_store': args.input_store,
'non_interactive': args.non_interactive,
})
}, mappings=mappings, summary=summary, print_summary=False)
elif isinstance(content['struct'], list):
for struct in content['struct']:
self._create_structure({
'structure_definition': struct,
'base_path': folder_path,
'structures_path': args.structures_path,
'dry_run': args.dry_run,
'diff': getattr(args, 'diff', False),
'output': getattr(args, 'output', 'file'),
'vars': merged_vars,
'backup': args.backup,
'file_strategy': args.file_strategy,
'global_system_prompt': args.global_system_prompt,
'input_store': args.input_store,
'non_interactive': args.non_interactive,
})
}, mappings=mappings, summary=summary, print_summary=False)
else:
self.logger.warning(f"Unsupported content in folder: {folder}")

# Final summary (only once for top-level call)
if print_summary:
self.logger.info("")
self.logger.info("Summary of actions:")
self.logger.info(f" ✅ Created: {summary['created']}")
self.logger.info(f" ✅ Updated: {summary['updated']}")
self.logger.info(f" 📝 Appended: {summary['appended']}")
self.logger.info(f" ⏭️ Skipped: {summary['skipped']}")
self.logger.info(f" 🗄️ Backed up: {summary['backed_up']}")
self.logger.info(f" 🔁 Renamed: {summary['renamed']}")
self.logger.info(f" 📁 Folders created: {summary['folders']}")
if args.dry_run:
self.logger.info(
f" [DRY RUN] Would create: {summary['dry_run_created']}")
self.logger.info(
f" [DRY RUN] Would update: {summary['dry_run_updated']}")

return summary
64 changes: 45 additions & 19 deletions struct_module/file_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def __init__(self, properties):
self.non_interactive,
self.mappings
)
# internal flags used for reporting
self._last_action = None

def _get_file_directory(self):
return os.path.dirname(self.name)
Expand Down Expand Up @@ -110,47 +112,71 @@ def create(self, base_path, dry_run=False, backup_path=None, file_strategy='over

file_path = self.template_renderer.render_template(file_path, self.vars)

# default result
result = {"action": None, "path": file_path}

if self.skip:
self.logger.info(f"skip is set to true. skipping creation.")
return
self.logger.info(f"⏭️ Skipped (skip=true): {file_path}")
result["action"] = "skipped"
return result

if dry_run:
self.logger.info(f"[DRY RUN] Would create file: {file_path} with content: \n\n{self.content}")
return
self.logger.info(f"[DRY RUN] Would create/update: {file_path}")
result["action"] = "dry_run"
return result

if self.skip_if_exists and os.path.exists(file_path):
self.logger.info(f" skip_if_exists is set to true and file already exists. skipping creation.")
return
self.logger.info(f"⏭️ Skipped (exists and skip_if_exists=true): {file_path}")
result["action"] = "skipped"
return result

# Create the directory if it does not exist
os.makedirs(os.path.dirname(file_path), exist_ok=True)

if os.path.exists(file_path):
existed_before = os.path.exists(file_path)
renamed_from = None
backed_up_to = None

if existed_before:
if file_strategy == 'backup' and backup_path:
backup_file_path = os.path.join(backup_path, os.path.basename(file_path))
shutil.copy2(file_path, backup_file_path)
self.logger.info(f"Backed up existing file: {file_path} to {backup_file_path}")
backed_up_to = backup_file_path
self.logger.info(f"🗄️ Backed up: {file_path} -> {backup_file_path}")
elif file_strategy == 'skip':
self.logger.info(f"Skipped existing file: {file_path}")
return
self.logger.info(f"⏭️ Skipped (exists): {file_path}")
result["action"] = "skipped"
return result
elif file_strategy == 'append':
with open(file_path, 'a') as f:
f.write(f"{self.content}\n")
self.logger.info(f"✅ Appended to existing file: {file_path}")
return
self.logger.info(f"📝 Appended: {file_path}")
result.update({"action": "appended"})
return result
elif file_strategy == 'rename':
new_name = f"{file_path}.{int(time.time())}"
os.rename(file_path, new_name)
self.logger.info(f"Renamed existing file: {file_path} to {new_name}")
renamed_from = new_name
self.logger.info(f"🔁 Renamed: {file_path} -> {new_name}")

# Write/overwrite the file
with open(file_path, 'w') as f:
f.write(f"{self.content}\n")
self.logger.info(f"✅ Created file with content")
self.logger.info(f" File path: {file_path}")
self.logger.debug(f" Content: \n\n{self.content}")

action = "created" if not existed_before else "updated"
if action == "created":
self.logger.info(f"✅ Created: {file_path}")
else:
self.logger.info(f"✅ Updated: {file_path}")
self.logger.debug(f"Content: \n\n{self.content}")

if self.permissions:
os.chmod(file_path, int(self.permissions, 8))
self.logger.info(f"🔐 Set permissions to file")
self.logger.info(f" File path: {file_path}")
self.logger.info(f" Permissions: {self.permissions}")
self.logger.info(f"🔐 Set permissions: {self.permissions} on {file_path}")

result.update({
"action": action,
"renamed_from": renamed_from,
"backed_up_to": backed_up_to,
})
return result
23 changes: 21 additions & 2 deletions struct_module/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,27 @@ async def _handle_get_structure_info(self, arguments: Dict[str, Any]) -> CallToo

if config.get('folders'):
result_text += " 📌 Folders:\n"
for folder in config.get('folders', []):
result_text += f" - {folder}\n"
for item in config.get('folders', []):
if isinstance(item, dict):
for folder, content in item.items():
result_text += f" - {folder}\n"
if isinstance(content, dict):
if 'struct' in content:
structs = content['struct']
if isinstance(structs, list):
result_text += " • struct(s):\n"
for s in structs:
result_text += f" - {s}\n"
elif isinstance(structs, str):
result_text += f" • struct: {structs}\n"
if 'with' in content and isinstance(content['with'], dict):
result_text += " • with:"
for k, v in content['with'].items():
result_text += f" {k}={v}"
result_text += "\n"
else:
# Fallback if item isn't a dict
result_text += f" - {item}\n"

return CallToolResult(
content=[
Expand Down
Loading
Loading