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
48 changes: 39 additions & 9 deletions struct_module/commands/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def __init__(self, parser):
parser.add_argument('-s', '--structures-path', type=str, help='Path to structure definitions')
parser.add_argument('-n', '--input-store', type=str, help='Path to the input store', default='/tmp/struct/input.json')
parser.add_argument('-d', '--dry-run', action='store_true', help='Perform a dry run without creating any files or directories')
parser.add_argument('--diff', action='store_true', help='Show unified diffs for files that would change during dry-run or console output')
parser.add_argument('-v', '--vars', type=str, help='Template variables in the format KEY1=value1,KEY2=value2')
parser.add_argument('-b', '--backup', type=str, help='Path to the backup folder')
parser.add_argument('-f', '--file-strategy', type=str, choices=['overwrite', 'skip', 'append', 'rename', 'backup'], default='overwrite', help='Strategy for handling existing files').completer = file_strategy_completer
Expand Down Expand Up @@ -189,18 +190,47 @@ def _create_structure(self, args, mappings=None):
)
file_item.apply_template_variables(template_vars)

# Output mode logic
# Output mode logic with diff support
if hasattr(args, 'output') and args.output == 'console':
# Print the file path and content to the console instead of creating the file
print(f"=== {file_path_to_create} ===")
print(file_item.content)
if args.diff and existing_content is not None:
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.endswith("\n") else existing_content + "\n"
diff = difflib.unified_diff(
old_content.splitlines(keepends=True),
new_content.splitlines(keepends=True),
fromfile=f"a/{file_path_to_create}",
tofile=f"b/{file_path_to_create}",
)
print("".join(diff))
else:
print(file_item.content)
else:
file_item.create(
args.base_path,
args.dry_run or False,
args.backup or None,
args.file_strategy or 'overwrite'
)
# When dry-run with --diff and files mode, print action and diff instead of writing
if args.dry_run and args.diff:
action = "create"
if existing_content is not None:
action = "update"
print(f"[DRY RUN] {action}: {file_path_to_create}")
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 "")
old_content = old_content if old_content.endswith("\n") else (old_content + ("\n" if old_content else ""))
diff = difflib.unified_diff(
old_content.splitlines(keepends=True),
new_content.splitlines(keepends=True),
fromfile=f"a/{file_path_to_create}",
tofile=f"b/{file_path_to_create}",
)
print("".join(diff))
else:
file_item.create(
args.base_path,
args.dry_run or False,
args.backup or None,
args.file_strategy or 'overwrite'
)

for item in config_folders:
for folder, content in item.items():
Expand Down
38 changes: 38 additions & 0 deletions tests/test_commands_more.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,44 @@ def test_generate_creates_base_path_and_console_output(parser, tmp_path):
mock_makedirs.assert_called() # base path created


def test_generate_dry_run_diff_shows_unified_diff(parser, tmp_path):
command = GenerateCommand(parser)
args = parser.parse_args(['struct-x', str(tmp_path / 'base')])

# Minimal config to trigger one file update
config = {'files': [{'hello.txt': 'Hello world'}], 'folders': []}

# Existing file with different content
base_dir = tmp_path / 'base'
base_dir.mkdir(parents=True, exist_ok=True)
(base_dir / 'hello.txt').write_text('Hello old\n')

store_dir = tmp_path / 'store'
store_dir.mkdir(parents=True, exist_ok=True)
with open(store_dir / 'input.json', 'w') as fh:
fh.write('{}')

with patch.object(command, '_load_yaml_config', return_value=config), \
patch('builtins.print') as mock_print:
args.output = 'file'
args.input_store = str(store_dir / 'input.json')
args.dry_run = True
args.diff = True
args.vars = None
args.backup = None
args.file_strategy = 'overwrite'
args.global_system_prompt = None
args.structures_path = None
args.non_interactive = True

command.execute(args)

# Should have printed a DRY RUN action and diff
printed = ''.join(call.args[0] for call in mock_print.call_args_list)
assert '[DRY RUN] update' in printed
assert '--- a' in printed and '+++ b' in printed


def test_generate_pre_hook_failure_aborts(parser, tmp_path):
command = GenerateCommand(parser)
args = parser.parse_args(['struct-x', str(tmp_path)])
Expand Down
Loading