Skip to content

Commit 67d3eb8

Browse files
authored
Improve CLI messages and final summary; enhance MCP structure info (fixes #99) (#109)
This PR addresses #99 by improving clarity and consistency of logs and user-facing messages, adding a final run summary, and enhancing the MCP server get_structure_info output. Changes - Standardize file operation messages across create/update/append/skip/backup/rename with emojis and clear paths. - Add final summary counts for actions (created, updated, appended, skipped, backed up, renamed, folders) and dry-run counts when applicable. - Improve get_structure_info to render folders and nested struct/with blocks cleanly rather than raw dicts. - Keep all existing tests passing (107/107). Acceptance criteria - Output is consistent and easy to scan; final summary present. - get_structure_info produces clean listings. - No breaking changes to existing behavior, tests updated not required. Screenshots/GIFs - Docs update for screenshots can be handled separately if desired.
1 parent a60e222 commit 67d3eb8

5 files changed

Lines changed: 404 additions & 30 deletions

File tree

struct_module/commands/generate.py

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,21 +136,35 @@ def execute(self, args):
136136
self.logger.error("Post-hook failed.")
137137
return
138138

139-
def _create_structure(self, args, mappings=None):
139+
def _create_structure(self, args, mappings=None, summary=None, print_summary=True):
140140
if isinstance(args, dict):
141141
args = argparse.Namespace(**args)
142142
this_file = os.path.dirname(os.path.realpath(__file__))
143143
contribs_path = os.path.join(this_file, "..", "contribs")
144144

145145
config = self._load_yaml_config(args.structure_definition, args.structures_path)
146146
if config is None:
147-
return
147+
return summary if summary is not None else None
148148

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

154+
# Action counters for final summary (initialize once and reuse across recursive calls)
155+
if summary is None:
156+
summary = {
157+
"created": 0,
158+
"updated": 0,
159+
"appended": 0,
160+
"skipped": 0,
161+
"backed_up": 0,
162+
"renamed": 0,
163+
"folders": 0,
164+
"dry_run_created": 0,
165+
"dry_run_updated": 0,
166+
}
167+
154168
for item in config_structure:
155169
self.logger.debug(f"Processing item: {item}")
156170
for name, content in item.items():
@@ -180,7 +194,7 @@ def _create_structure(self, args, mappings=None):
180194
file_path_to_create = os.path.join(args.base_path, name)
181195
existing_content = None
182196
if os.path.exists(file_path_to_create):
183-
self.logger.warning(f"⚠️ File already exists: {file_path_to_create}")
197+
self.logger.info(f"ℹ️ Exists: {file_path_to_create}")
184198
with open(file_path_to_create, 'r') as existing_file:
185199
existing_content = existing_file.read()
186200

@@ -213,6 +227,10 @@ def _create_structure(self, args, mappings=None):
213227
if existing_content is not None:
214228
action = "update"
215229
print(f"[DRY RUN] {action}: {file_path_to_create}")
230+
if action == "create":
231+
summary["dry_run_created"] += 1
232+
else:
233+
summary["dry_run_updated"] += 1
216234
import difflib
217235
new_content = file_item.content if file_item.content.endswith("\n") else file_item.content + "\n"
218236
old_content = (existing_content if existing_content is not None else "")
@@ -225,26 +243,39 @@ def _create_structure(self, args, mappings=None):
225243
)
226244
print("".join(diff))
227245
else:
228-
file_item.create(
246+
result = file_item.create(
229247
args.base_path,
230248
args.dry_run or False,
231249
args.backup or None,
232250
args.file_strategy or 'overwrite'
233251
)
252+
if isinstance(result, dict):
253+
if result.get("action") == "created":
254+
summary["created"] += 1
255+
elif result.get("action") == "updated":
256+
summary["updated"] += 1
257+
elif result.get("action") == "appended":
258+
summary["appended"] += 1
259+
elif result.get("action") == "skipped":
260+
summary["skipped"] += 1
261+
if result.get("backed_up_to"):
262+
summary["backed_up"] += 1
263+
if result.get("renamed_from"):
264+
summary["renamed"] += 1
234265

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

243274
# check if content has struct value
244275
if 'struct' in content:
245276
self.logger.info(f"Generating structure")
246277
self.logger.info(f" Folder: {folder}")
247-
self.logger.info(f" Struct:")
278+
self.logger.info(f" Struct(s):")
248279
if isinstance(content['struct'], list):
249280
# iterate over the list of structures
250281
for struct in content['struct']:
@@ -279,26 +310,49 @@ def _create_structure(self, args, mappings=None):
279310
'base_path': folder_path,
280311
'structures_path': args.structures_path,
281312
'dry_run': args.dry_run,
313+
'diff': getattr(args, 'diff', False),
314+
'output': getattr(args, 'output', 'file'),
282315
'vars': merged_vars,
283316
'backup': args.backup,
284317
'file_strategy': args.file_strategy,
285318
'global_system_prompt': args.global_system_prompt,
286319
'input_store': args.input_store,
287320
'non_interactive': args.non_interactive,
288-
})
321+
}, mappings=mappings, summary=summary, print_summary=False)
289322
elif isinstance(content['struct'], list):
290323
for struct in content['struct']:
291324
self._create_structure({
292325
'structure_definition': struct,
293326
'base_path': folder_path,
294327
'structures_path': args.structures_path,
295328
'dry_run': args.dry_run,
329+
'diff': getattr(args, 'diff', False),
330+
'output': getattr(args, 'output', 'file'),
296331
'vars': merged_vars,
297332
'backup': args.backup,
298333
'file_strategy': args.file_strategy,
299334
'global_system_prompt': args.global_system_prompt,
300335
'input_store': args.input_store,
301336
'non_interactive': args.non_interactive,
302-
})
337+
}, mappings=mappings, summary=summary, print_summary=False)
303338
else:
304339
self.logger.warning(f"Unsupported content in folder: {folder}")
340+
341+
# Final summary (only once for top-level call)
342+
if print_summary:
343+
self.logger.info("")
344+
self.logger.info("Summary of actions:")
345+
self.logger.info(f" ✅ Created: {summary['created']}")
346+
self.logger.info(f" ✅ Updated: {summary['updated']}")
347+
self.logger.info(f" 📝 Appended: {summary['appended']}")
348+
self.logger.info(f" ⏭️ Skipped: {summary['skipped']}")
349+
self.logger.info(f" 🗄️ Backed up: {summary['backed_up']}")
350+
self.logger.info(f" 🔁 Renamed: {summary['renamed']}")
351+
self.logger.info(f" 📁 Folders created: {summary['folders']}")
352+
if args.dry_run:
353+
self.logger.info(
354+
f" [DRY RUN] Would create: {summary['dry_run_created']}")
355+
self.logger.info(
356+
f" [DRY RUN] Would update: {summary['dry_run_updated']}")
357+
358+
return summary

struct_module/file_item.py

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ def __init__(self, properties):
3939
self.non_interactive,
4040
self.mappings
4141
)
42+
# internal flags used for reporting
43+
self._last_action = None
4244

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

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

115+
# default result
116+
result = {"action": None, "path": file_path}
117+
113118
if self.skip:
114-
self.logger.info(f"skip is set to true. skipping creation.")
115-
return
119+
self.logger.info(f"⏭️ Skipped (skip=true): {file_path}")
120+
result["action"] = "skipped"
121+
return result
116122

117123
if dry_run:
118-
self.logger.info(f"[DRY RUN] Would create file: {file_path} with content: \n\n{self.content}")
119-
return
124+
self.logger.info(f"[DRY RUN] Would create/update: {file_path}")
125+
result["action"] = "dry_run"
126+
return result
120127

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

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

128-
if os.path.exists(file_path):
136+
existed_before = os.path.exists(file_path)
137+
renamed_from = None
138+
backed_up_to = None
139+
140+
if existed_before:
129141
if file_strategy == 'backup' and backup_path:
130142
backup_file_path = os.path.join(backup_path, os.path.basename(file_path))
131143
shutil.copy2(file_path, backup_file_path)
132-
self.logger.info(f"Backed up existing file: {file_path} to {backup_file_path}")
144+
backed_up_to = backup_file_path
145+
self.logger.info(f"🗄️ Backed up: {file_path} -> {backup_file_path}")
133146
elif file_strategy == 'skip':
134-
self.logger.info(f"Skipped existing file: {file_path}")
135-
return
147+
self.logger.info(f"⏭️ Skipped (exists): {file_path}")
148+
result["action"] = "skipped"
149+
return result
136150
elif file_strategy == 'append':
137151
with open(file_path, 'a') as f:
138152
f.write(f"{self.content}\n")
139-
self.logger.info(f"✅ Appended to existing file: {file_path}")
140-
return
153+
self.logger.info(f"📝 Appended: {file_path}")
154+
result.update({"action": "appended"})
155+
return result
141156
elif file_strategy == 'rename':
142157
new_name = f"{file_path}.{int(time.time())}"
143158
os.rename(file_path, new_name)
144-
self.logger.info(f"Renamed existing file: {file_path} to {new_name}")
159+
renamed_from = new_name
160+
self.logger.info(f"🔁 Renamed: {file_path} -> {new_name}")
145161

162+
# Write/overwrite the file
146163
with open(file_path, 'w') as f:
147164
f.write(f"{self.content}\n")
148-
self.logger.info(f"✅ Created file with content")
149-
self.logger.info(f" File path: {file_path}")
150-
self.logger.debug(f" Content: \n\n{self.content}")
165+
166+
action = "created" if not existed_before else "updated"
167+
if action == "created":
168+
self.logger.info(f"✅ Created: {file_path}")
169+
else:
170+
self.logger.info(f"✅ Updated: {file_path}")
171+
self.logger.debug(f"Content: \n\n{self.content}")
151172

152173
if self.permissions:
153174
os.chmod(file_path, int(self.permissions, 8))
154-
self.logger.info(f"🔐 Set permissions to file")
155-
self.logger.info(f" File path: {file_path}")
156-
self.logger.info(f" Permissions: {self.permissions}")
175+
self.logger.info(f"🔐 Set permissions: {self.permissions} on {file_path}")
176+
177+
result.update({
178+
"action": action,
179+
"renamed_from": renamed_from,
180+
"backed_up_to": backed_up_to,
181+
})
182+
return result

struct_module/mcp_server.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,27 @@ async def _handle_get_structure_info(self, arguments: Dict[str, Any]) -> CallToo
280280

281281
if config.get('folders'):
282282
result_text += " 📌 Folders:\n"
283-
for folder in config.get('folders', []):
284-
result_text += f" - {folder}\n"
283+
for item in config.get('folders', []):
284+
if isinstance(item, dict):
285+
for folder, content in item.items():
286+
result_text += f" - {folder}\n"
287+
if isinstance(content, dict):
288+
if 'struct' in content:
289+
structs = content['struct']
290+
if isinstance(structs, list):
291+
result_text += " • struct(s):\n"
292+
for s in structs:
293+
result_text += f" - {s}\n"
294+
elif isinstance(structs, str):
295+
result_text += f" • struct: {structs}\n"
296+
if 'with' in content and isinstance(content['with'], dict):
297+
result_text += " • with:"
298+
for k, v in content['with'].items():
299+
result_text += f" {k}={v}"
300+
result_text += "\n"
301+
else:
302+
# Fallback if item isn't a dict
303+
result_text += f" - {item}\n"
285304

286305
return CallToolResult(
287306
content=[

0 commit comments

Comments
 (0)