claude-dev-cli 0.12.1__py3-none-any.whl → 0.13.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-dev-cli might be problematic. Click here for more details.

@@ -9,7 +9,7 @@ Features:
9
9
  - Interactive and single-shot modes
10
10
  """
11
11
 
12
- __version__ = "0.12.1"
12
+ __version__ = "0.13.3"
13
13
  __author__ = "Julio"
14
14
  __license__ = "MIT"
15
15
 
claude_dev_cli/cli.py CHANGED
@@ -1065,12 +1065,14 @@ def gen_docs(
1065
1065
  @click.option('-f', '--file', 'spec_file', type=click.Path(exists=True), help='Read specification from file')
1066
1066
  @click.option('--pdf', type=click.Path(exists=True), help='Read specification from PDF')
1067
1067
  @click.option('--url', help='Fetch specification from URL')
1068
- @click.option('-o', '--output', required=True, type=click.Path(), help='Output file path (required)')
1068
+ @click.option('-o', '--output', required=True, type=click.Path(), help='Output file or directory path (required)')
1069
1069
  @click.option('--language', help='Target language (auto-detected from output extension)')
1070
1070
  @click.option('-m', '--model', help='Model profile to use')
1071
1071
  @click.option('-a', '--api', help='API config to use')
1072
1072
  @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
1073
1073
  @click.option('--auto-context', is_flag=True, help='Include project context')
1074
+ @click.option('--dry-run', is_flag=True, help='Preview without writing files')
1075
+ @click.option('--yes', '-y', is_flag=True, help='Skip confirmation prompts')
1074
1076
  @click.pass_context
1075
1077
  def gen_code(
1076
1078
  ctx: click.Context,
@@ -1083,17 +1085,25 @@ def gen_code(
1083
1085
  model: Optional[str],
1084
1086
  api: Optional[str],
1085
1087
  interactive: bool,
1086
- auto_context: bool
1088
+ auto_context: bool,
1089
+ dry_run: bool,
1090
+ yes: bool
1087
1091
  ) -> None:
1088
1092
  """Generate code from a specification.
1089
1093
 
1090
1094
  Reads specification from text, file, PDF, or URL and generates complete code.
1091
1095
 
1092
- Examples:
1096
+ Single file mode (output is file):
1093
1097
  cdc generate code --description "REST API client" -o client.py
1098
+
1099
+ Multi-file mode (output is directory):
1100
+ cdc generate code --description "FastAPI app" -o my-api/
1101
+ cdc generate code --file spec.md -o project/
1102
+
1103
+ Examples:
1094
1104
  cdc generate code --file spec.md -o implementation.go
1095
1105
  cdc generate code --pdf requirements.pdf -o app.js
1096
- cdc generate code --url https://example.com/spec -o service.py
1106
+ cdc generate code --url https://example.com/spec -o service/ --dry-run
1097
1107
  """
1098
1108
  console = ctx.obj['console']
1099
1109
  from claude_dev_cli.input_sources import get_input_content
@@ -1129,18 +1139,51 @@ def gen_code(
1129
1139
  }
1130
1140
  language = language_map.get(ext, ext.upper() if ext else None)
1131
1141
 
1142
+ # Detect if output is directory (multi-file mode)
1143
+ output_path = Path(output)
1144
+ is_directory = output.endswith('/') or output_path.is_dir()
1145
+
1132
1146
  console.print(f"[cyan]Generating code from:[/cyan] {source_desc}")
1133
1147
  if language:
1134
1148
  console.print(f"[cyan]Target language:[/cyan] {language}")
1135
- console.print(f"[cyan]Output:[/cyan] {output}\n")
1136
1149
 
1137
- # Build prompt
1138
- prompt = f"Specification:\n\n{spec_content}\n\n"
1139
- if language:
1140
- prompt += f"Generate complete, production-ready {language} code that implements this specification. "
1150
+ if is_directory:
1151
+ console.print(f"[cyan]Output directory:[/cyan] {output}")
1152
+ console.print(f"[cyan]Mode:[/cyan] Multi-file project generation\n")
1153
+ else:
1154
+ console.print(f"[cyan]Output file:[/cyan] {output}\n")
1155
+
1156
+ # Build prompt (different for single vs multi-file)
1157
+ if is_directory:
1158
+ # Multi-file mode: request structured output
1159
+ prompt = f"Specification:\n\n{spec_content}\n\n"
1160
+ if language:
1161
+ prompt += f"Generate a complete, production-ready {language} project that implements this specification. "
1162
+ else:
1163
+ prompt += "Generate a complete, production-ready project that implements this specification. "
1164
+ prompt += """\n
1165
+ Provide your response in this format:
1166
+
1167
+ ## File: relative/path/to/file.ext
1168
+ ```language
1169
+ // Complete file content
1170
+ ```
1171
+
1172
+ ## File: another/file.ext
1173
+ ```language
1174
+ // Complete file content
1175
+ ```
1176
+
1177
+ Include ALL necessary files: source code, configuration, dependencies, README, etc.
1178
+ Use proper directory structure and include proper error handling, documentation, and best practices."""
1141
1179
  else:
1142
- prompt += "Generate complete, production-ready code that implements this specification. "
1143
- prompt += "Include proper error handling, documentation, and best practices."
1180
+ # Single file mode: simple prompt
1181
+ prompt = f"Specification:\n\n{spec_content}\n\n"
1182
+ if language:
1183
+ prompt += f"Generate complete, production-ready {language} code that implements this specification. "
1184
+ else:
1185
+ prompt += "Generate complete, production-ready code that implements this specification. "
1186
+ prompt += "Include proper error handling, documentation, and best practices."
1144
1187
 
1145
1188
  # Add context if requested
1146
1189
  if auto_context:
@@ -1197,8 +1240,51 @@ def gen_code(
1197
1240
  conversation_context.append(result)
1198
1241
 
1199
1242
  # Save output
1200
- Path(output).write_text(result)
1201
- console.print(f"\n[green]✓[/green] Code saved to: {output}")
1243
+ if is_directory:
1244
+ # Multi-file mode: parse and write multiple files
1245
+ from claude_dev_cli.multi_file_handler import MultiFileResponse
1246
+
1247
+ multi_file = MultiFileResponse()
1248
+ multi_file.parse_response(result, base_path=output_path)
1249
+
1250
+ if not multi_file.files:
1251
+ console.print("[yellow]Warning: No files were detected in the response.[/yellow]")
1252
+ console.print("[dim]Falling back to single file output...[/dim]")
1253
+ # Save as single file anyway
1254
+ fallback_file = output_path / "generated_code.txt"
1255
+ output_path.mkdir(parents=True, exist_ok=True)
1256
+ fallback_file.write_text(result)
1257
+ console.print(f"\n[green]✓[/green] Output saved to: {fallback_file}")
1258
+ return
1259
+
1260
+ # Validate paths
1261
+ errors = multi_file.validate_paths(output_path)
1262
+ if errors:
1263
+ console.print("[red]Path validation errors:[/red]")
1264
+ for error in errors:
1265
+ console.print(f" • {error}")
1266
+ sys.exit(1)
1267
+
1268
+ # Show preview
1269
+ multi_file.preview(console, output_path)
1270
+
1271
+ # Confirm or auto-accept
1272
+ if not yes and not dry_run:
1273
+ if not multi_file.confirm(console, output_path):
1274
+ console.print("[yellow]Cancelled[/yellow]")
1275
+ return
1276
+
1277
+ # Write files
1278
+ multi_file.write_all(output_path, dry_run=dry_run, console=console)
1279
+
1280
+ if dry_run:
1281
+ console.print("\n[yellow]Dry-run mode - no files were written[/yellow]")
1282
+ else:
1283
+ console.print(f"\n[green]✓[/green] Project created in: {output}")
1284
+ else:
1285
+ # Single file mode: simple write
1286
+ Path(output).write_text(result)
1287
+ console.print(f"\n[green]✓[/green] Code saved to: {output}")
1202
1288
 
1203
1289
  except Exception as e:
1204
1290
  console.print(f"[red]Error: {e}[/red]")
@@ -1217,6 +1303,8 @@ def gen_code(
1217
1303
  @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
1218
1304
  @click.option('--auto-context', is_flag=True, help='Include project context')
1219
1305
  @click.option('--preview', is_flag=True, help='Preview changes without applying')
1306
+ @click.option('--dry-run', is_flag=True, help='Show what would be changed without writing')
1307
+ @click.option('--yes', '-y', is_flag=True, help='Apply changes without confirmation')
1220
1308
  @click.pass_context
1221
1309
  def gen_feature(
1222
1310
  ctx: click.Context,
@@ -1230,7 +1318,9 @@ def gen_feature(
1230
1318
  api: Optional[str],
1231
1319
  interactive: bool,
1232
1320
  auto_context: bool,
1233
- preview: bool
1321
+ preview: bool,
1322
+ dry_run: bool,
1323
+ yes: bool
1234
1324
  ) -> None:
1235
1325
  """Generate code to add a feature to existing project.
1236
1326
 
@@ -1241,6 +1331,8 @@ def gen_feature(
1241
1331
  cdc generate feature --file feature-spec.md
1242
1332
  cdc generate feature --pdf requirements.pdf --preview
1243
1333
  cdc generate feature --url https://example.com/spec src/
1334
+ cdc generate feature -f spec.md --dry-run
1335
+ cdc generate feature -f spec.md --yes
1244
1336
  """
1245
1337
  console = ctx.obj['console']
1246
1338
  from claude_dev_cli.input_sources import get_input_content
@@ -1290,15 +1382,19 @@ def gen_feature(
1290
1382
  except Exception as e:
1291
1383
  console.print(f"[yellow]Warning: Could not read {file_path}: {e}[/yellow]")
1292
1384
 
1293
- # Build prompt
1385
+ # Build prompt with multi-file support
1294
1386
  prompt = f"Feature Specification:\n\n{spec_content}\n\n"
1295
1387
  prompt += f"Existing Codebase:{codebase_content}\n\n"
1296
- prompt += "Analyze the existing code and provide:\n"
1297
- prompt += "1. Implementation plan for the feature\n"
1298
- prompt += "2. List of files to modify or create\n"
1299
- prompt += "3. Complete code changes (diffs or new files)\n"
1300
- prompt += "4. Any necessary setup or configuration changes\n\n"
1301
- prompt += "Be specific and provide complete, working code."
1388
+ prompt += "Analyze the existing code and provide the complete implementation.\n\n"
1389
+ prompt += "IMPORTANT: Structure your response with file markers:\n"
1390
+ prompt += "## File: path/to/file.ext\n"
1391
+ prompt += "```language\n"
1392
+ prompt += "// complete file content\n"
1393
+ prompt += "```\n\n"
1394
+ prompt += "Use '## Create: path/to/new.ext' for new files.\n"
1395
+ prompt += "Use '## Modify: path/to/existing.ext' for changes to existing files.\n"
1396
+ prompt += "Use '## Delete: path/to/old.ext' if files should be removed.\n\n"
1397
+ prompt += "Provide complete, working code for all affected files."
1302
1398
 
1303
1399
  # Add context if requested
1304
1400
  if auto_context:
@@ -1317,39 +1413,32 @@ def gen_feature(
1317
1413
  client = ClaudeClient(api_config_name=api)
1318
1414
  result = client.call(prompt, model=model)
1319
1415
 
1320
- # Show result
1321
- from rich.markdown import Markdown
1322
- md = Markdown(result)
1323
- console.print(md)
1324
-
1325
- if preview:
1326
- console.print("\n[yellow]Preview mode - no changes applied[/yellow]")
1327
- console.print("[dim]Remove --preview flag to apply changes[/dim]")
1328
- return
1329
-
1330
1416
  # Interactive refinement
1331
1417
  if interactive:
1418
+ from rich.markdown import Markdown
1419
+ md = Markdown(result)
1420
+ console.print(md)
1421
+
1332
1422
  client = ClaudeClient(api_config_name=api)
1333
1423
  conversation_context = [result]
1334
1424
 
1335
1425
  while True:
1336
- console.print("\n[dim]Ask for changes, 'apply' to confirm, or 'exit' to cancel[/dim]")
1426
+ console.print("\n[dim]Ask for changes, 'save' to continue, or 'exit' to cancel[/dim]")
1337
1427
  user_input = console.input("[cyan]You:[/cyan] ").strip()
1338
1428
 
1339
1429
  if user_input.lower() == 'exit':
1340
1430
  console.print("[yellow]Cancelled[/yellow]")
1341
1431
  return
1342
1432
 
1343
- if user_input.lower() == 'apply':
1344
- console.print("[green]✓[/green] Implementation plan ready")
1345
- console.print("[dim]Apply the changes manually from the output above[/dim]")
1346
- return
1433
+ if user_input.lower() == 'save':
1434
+ result = conversation_context[-1]
1435
+ break
1347
1436
 
1348
1437
  if not user_input:
1349
1438
  continue
1350
1439
 
1351
1440
  # Get refinement
1352
- refinement_prompt = f"Previous implementation plan:\n\n{conversation_context[-1]}\n\nUser request: {user_input}\n\nProvide the updated implementation."
1441
+ refinement_prompt = f"Previous implementation:\n\n{conversation_context[-1]}\n\nUser request: {user_input}\n\nProvide the updated implementation with file markers."
1353
1442
 
1354
1443
  console.print("\n[bold green]Claude:[/bold green] ", end='')
1355
1444
  response_parts = []
@@ -1360,9 +1449,54 @@ def gen_feature(
1360
1449
 
1361
1450
  result = ''.join(response_parts)
1362
1451
  conversation_context.append(result)
1363
- else:
1364
- console.print("\n[green]✓[/green] Feature implementation generated")
1365
- console.print("[dim]Apply the changes manually from the output above[/dim]")
1452
+
1453
+ # Parse multi-file response
1454
+ from claude_dev_cli.multi_file_handler import MultiFileResponse
1455
+ from pathlib import Path
1456
+
1457
+ # Use current directory as base
1458
+ base_path = Path.cwd()
1459
+
1460
+ multi_file = MultiFileResponse()
1461
+ multi_file.parse_response(result, base_path=base_path)
1462
+
1463
+ if not multi_file.files:
1464
+ # No structured output detected, show markdown
1465
+ console.print("\n[yellow]No structured file output detected[/yellow]")
1466
+ from rich.markdown import Markdown
1467
+ md = Markdown(result)
1468
+ console.print(md)
1469
+ console.print("\n[dim]Apply the changes manually from the output above[/dim]")
1470
+ return
1471
+
1472
+ # Validate paths
1473
+ errors = multi_file.validate_paths(base_path)
1474
+ if errors:
1475
+ console.print("[red]Path validation errors:[/red]")
1476
+ for error in errors:
1477
+ console.print(f" • {error}")
1478
+ sys.exit(1)
1479
+
1480
+ # Show preview
1481
+ multi_file.preview(console, base_path)
1482
+
1483
+ # Handle preview/dry-run modes
1484
+ if preview or dry_run:
1485
+ console.print("\n[yellow]Preview mode - no changes applied[/yellow]")
1486
+ if preview:
1487
+ console.print("[dim]Remove --preview flag to apply changes[/dim]")
1488
+ return
1489
+
1490
+ # Confirm or auto-accept
1491
+ if not yes:
1492
+ if not multi_file.confirm(console, base_path):
1493
+ console.print("[yellow]Cancelled[/yellow]")
1494
+ return
1495
+
1496
+ # Write files
1497
+ multi_file.write_all(base_path, dry_run=False, console=console)
1498
+
1499
+ console.print(f"\n[green]✓[/green] Feature implemented successfully")
1366
1500
 
1367
1501
  except Exception as e:
1368
1502
  console.print(f"[red]Error: {e}[/red]")
@@ -1541,6 +1675,9 @@ def debug(
1541
1675
  @click.option('-i', '--interactive', is_flag=True, help='Interactive refinement mode')
1542
1676
  @click.option('--auto-context', is_flag=True, help='Automatically include git, dependencies, and related files')
1543
1677
  @click.option('--max-files', type=int, default=20, help='Maximum files to refactor (default: 20)')
1678
+ @click.option('--preview', is_flag=True, help='Preview changes without applying')
1679
+ @click.option('--dry-run', is_flag=True, help='Show what would be changed without writing')
1680
+ @click.option('--yes', '-y', is_flag=True, help='Apply changes without confirmation')
1544
1681
  @click.pass_context
1545
1682
  def refactor(
1546
1683
  ctx: click.Context,
@@ -1549,7 +1686,10 @@ def refactor(
1549
1686
  api: Optional[str],
1550
1687
  interactive: bool,
1551
1688
  auto_context: bool,
1552
- max_files: int
1689
+ max_files: int,
1690
+ preview: bool,
1691
+ dry_run: bool,
1692
+ yes: bool
1553
1693
  ) -> None:
1554
1694
  """Suggest refactoring improvements.
1555
1695
 
@@ -1558,6 +1698,8 @@ def refactor(
1558
1698
  cdc refactor file1.py file2.py # Multiple files
1559
1699
  cdc refactor src/ # Directory
1560
1700
  cdc refactor # Auto-detect git changes
1701
+ cdc refactor src/ --dry-run # Preview changes
1702
+ cdc refactor file.py --yes # Apply without confirmation
1561
1703
  """
1562
1704
  console = ctx.obj['console']
1563
1705
  from claude_dev_cli.path_utils import expand_paths, auto_detect_files
@@ -1589,7 +1731,7 @@ def refactor(
1589
1731
  console.print(f" ... and {len(files) - 10} more")
1590
1732
  console.print()
1591
1733
 
1592
- # Build combined prompt
1734
+ # Build combined prompt with multi-file support
1593
1735
  files_content = ""
1594
1736
  for file_path in files:
1595
1737
  try:
@@ -1599,6 +1741,16 @@ def refactor(
1599
1741
  except Exception as e:
1600
1742
  console.print(f"[yellow]Warning: Could not read {file_path}: {e}[/yellow]")
1601
1743
 
1744
+ refactor_instructions = (
1745
+ "\n\nIMPORTANT: Structure your response with file markers:\n"
1746
+ "## File: path/to/file.ext\n"
1747
+ "```language\n"
1748
+ "// complete refactored file content\n"
1749
+ "```\n\n"
1750
+ "Use '## Modify: path/to/file.ext' to indicate files being refactored.\n"
1751
+ "Provide complete, working refactored code for all files.\n"
1752
+ )
1753
+
1602
1754
  # Gather context if requested
1603
1755
  if auto_context:
1604
1756
  from claude_dev_cli.context import ContextGatherer
@@ -1611,11 +1763,11 @@ def refactor(
1611
1763
  console.print("[dim]✓ Context gathered[/dim]")
1612
1764
 
1613
1765
  client = ClaudeClient(api_config_name=api)
1614
- prompt = f"{context_info}\n\nFiles:{files_content}\n\nPlease suggest refactoring improvements."
1766
+ prompt = f"{context_info}\n\nFiles:{files_content}\n\nPlease suggest refactoring improvements.{refactor_instructions}"
1615
1767
  else:
1616
1768
  with console.status(f"[bold blue]Analyzing {len(files)} file(s)..."):
1617
1769
  client = ClaudeClient(api_config_name=api)
1618
- prompt = f"Files to refactor:{files_content}\n\nPlease suggest refactoring improvements focusing on code quality, maintainability, and performance."
1770
+ prompt = f"Files to refactor:{files_content}\n\nPlease suggest refactoring improvements focusing on code quality, maintainability, and performance.{refactor_instructions}"
1619
1771
 
1620
1772
  result = client.call(prompt)
1621
1773
 
@@ -1627,7 +1779,7 @@ def refactor(
1627
1779
  conversation_context = [result]
1628
1780
 
1629
1781
  while True:
1630
- console.print("\n[dim]Commands: 'save' to save and exit, 'exit' to discard, or ask for changes[/dim]")
1782
+ console.print("\n[dim]Commands: 'save' to continue, 'exit' to discard, or ask for changes[/dim]")
1631
1783
  user_input = console.input("[cyan]You:[/cyan] ").strip()
1632
1784
 
1633
1785
  if user_input.lower() == 'exit':
@@ -1641,7 +1793,7 @@ def refactor(
1641
1793
  if not user_input:
1642
1794
  continue
1643
1795
 
1644
- refinement_prompt = f"Previous refactoring:\n\n{conversation_context[-1]}\n\nUser request: {user_input}\n\nProvide the updated refactoring suggestions."
1796
+ refinement_prompt = f"Previous refactoring:\n\n{conversation_context[-1]}\n\nUser request: {user_input}\n\nProvide the updated refactoring with file markers."
1645
1797
 
1646
1798
  console.print("\n[bold green]Claude:[/bold green] ", end='')
1647
1799
  response_parts = []
@@ -1653,13 +1805,64 @@ def refactor(
1653
1805
  result = ''.join(response_parts)
1654
1806
  conversation_context.append(result)
1655
1807
 
1808
+ # Handle single file output mode (legacy behavior)
1656
1809
  if output:
1657
- with open(output, 'w') as f:
1658
- f.write(result)
1659
- console.print(f"\n[green]✓[/green] Refactored code saved to: {output}")
1660
- elif not interactive:
1661
- md = Markdown(result)
1662
- console.print(md)
1810
+ if len(files) == 1:
1811
+ with open(output, 'w') as f:
1812
+ f.write(result)
1813
+ console.print(f"\n[green]✓[/green] Refactored code saved to: {output}")
1814
+ else:
1815
+ console.print("[yellow]Warning: --output only works with single file. Using multi-file mode.[/yellow]")
1816
+ output = None
1817
+
1818
+ # Parse multi-file response if output not specified
1819
+ if not output:
1820
+ from claude_dev_cli.multi_file_handler import MultiFileResponse
1821
+ from pathlib import Path
1822
+
1823
+ # Use current directory as base
1824
+ base_path = Path.cwd()
1825
+
1826
+ multi_file = MultiFileResponse()
1827
+ multi_file.parse_response(result, base_path=base_path)
1828
+
1829
+ if not multi_file.files:
1830
+ # No structured output detected, show markdown
1831
+ if not interactive:
1832
+ console.print("\n[yellow]No structured file output detected[/yellow]")
1833
+ md = Markdown(result)
1834
+ console.print(md)
1835
+ console.print("\n[dim]Apply the changes manually from the output above[/dim]")
1836
+ return
1837
+
1838
+ # Validate paths
1839
+ errors = multi_file.validate_paths(base_path)
1840
+ if errors:
1841
+ console.print("[red]Path validation errors:[/red]")
1842
+ for error in errors:
1843
+ console.print(f" • {error}")
1844
+ sys.exit(1)
1845
+
1846
+ # Show preview
1847
+ multi_file.preview(console, base_path)
1848
+
1849
+ # Handle preview/dry-run modes
1850
+ if preview or dry_run:
1851
+ console.print("\n[yellow]Preview mode - no changes applied[/yellow]")
1852
+ if preview:
1853
+ console.print("[dim]Remove --preview flag to apply changes[/dim]")
1854
+ return
1855
+
1856
+ # Confirm or auto-accept
1857
+ if not yes:
1858
+ if not multi_file.confirm(console, base_path):
1859
+ console.print("[yellow]Cancelled[/yellow]")
1860
+ return
1861
+
1862
+ # Write files
1863
+ multi_file.write_all(base_path, dry_run=False, console=console)
1864
+
1865
+ console.print(f"\n[green]✓[/green] Refactoring applied successfully")
1663
1866
 
1664
1867
  except Exception as e:
1665
1868
  console.print(f"[red]Error: {e}[/red]")
@@ -3,11 +3,50 @@
3
3
  import re
4
4
  import difflib
5
5
  from pathlib import Path
6
- from typing import List, Tuple, Optional, Literal
7
- from dataclasses import dataclass
6
+ from typing import List, Tuple, Optional, Literal, Dict, Any
7
+ from dataclasses import dataclass, field
8
8
  from rich.console import Console
9
9
  from rich.tree import Tree
10
10
  from rich.panel import Panel
11
+ from rich.syntax import Syntax
12
+ from io import StringIO
13
+
14
+ try:
15
+ from unidiff import PatchSet
16
+ UNIDIFF_AVAILABLE = True
17
+ except ImportError:
18
+ UNIDIFF_AVAILABLE = False
19
+
20
+
21
+ @dataclass
22
+ class HunkWrapper:
23
+ """Wrapper around unidiff.Hunk with approval state."""
24
+ hunk: Any # unidiff.Hunk
25
+ approved: bool = False
26
+
27
+ @property
28
+ def source_start(self) -> int:
29
+ """Get source start line number."""
30
+ return self.hunk.source_start if hasattr(self.hunk, 'source_start') else 0
31
+
32
+ @property
33
+ def source_length(self) -> int:
34
+ """Get source length."""
35
+ return self.hunk.source_length if hasattr(self.hunk, 'source_length') else 0
36
+
37
+ @property
38
+ def target_start(self) -> int:
39
+ """Get target start line number."""
40
+ return self.hunk.target_start if hasattr(self.hunk, 'target_start') else 0
41
+
42
+ @property
43
+ def target_length(self) -> int:
44
+ """Get target length."""
45
+ return self.hunk.target_length if hasattr(self.hunk, 'target_length') else 0
46
+
47
+ def __str__(self) -> str:
48
+ """Format hunk as unified diff text."""
49
+ return str(self.hunk)
11
50
 
12
51
 
13
52
  @dataclass
@@ -17,6 +56,7 @@ class FileChange:
17
56
  content: str
18
57
  change_type: Literal["create", "modify", "delete"]
19
58
  original_content: Optional[str] = None
59
+ hunks: List[HunkWrapper] = field(default_factory=list)
20
60
 
21
61
  @property
22
62
  def line_count(self) -> int:
@@ -36,11 +76,83 @@ class FileChange:
36
76
  original_lines,
37
77
  new_lines,
38
78
  fromfile=f"a/{self.path}",
39
- tofile=f"b/{self.path}",
40
- lineterm=''
79
+ tofile=f"b/{self.path}"
41
80
  ))
42
81
 
43
82
  return ''.join(diff_lines) if diff_lines else None
83
+
84
+ def parse_hunks(self) -> None:
85
+ """Parse diff into individual hunks using unidiff library."""
86
+ if self.change_type != "modify" or not self.diff:
87
+ return
88
+
89
+ if not UNIDIFF_AVAILABLE:
90
+ # Fallback: no hunk parsing available
91
+ self.hunks = []
92
+ return
93
+
94
+ self.hunks = []
95
+
96
+ try:
97
+ # Parse the diff with unidiff
98
+ patch = PatchSet(StringIO(self.diff))
99
+
100
+ # Extract all hunks from all patched files
101
+ for patched_file in patch:
102
+ for hunk in patched_file:
103
+ self.hunks.append(HunkWrapper(hunk=hunk, approved=False))
104
+
105
+ except Exception:
106
+ # If unidiff fails, fall back to empty hunks list
107
+ self.hunks = []
108
+
109
+ def apply_approved_hunks(self) -> str:
110
+ """Apply only approved hunks using unidiff's line access methods."""
111
+ if self.change_type != "modify" or not self.original_content:
112
+ return self.content
113
+
114
+ # If no hunks parsed, caller decides what to do
115
+ # In write_all, we check if hunks exist before calling this
116
+ if not self.hunks:
117
+ return self.content
118
+
119
+ # If no hunks approved, return original
120
+ if not any(h.approved for h in self.hunks):
121
+ return self.original_content
122
+
123
+ # If all hunks approved, return new content
124
+ if all(h.approved for h in self.hunks):
125
+ return self.content
126
+
127
+ # Apply only approved hunks
128
+ original_lines = self.original_content.splitlines(keepends=True)
129
+ result_lines = original_lines.copy()
130
+
131
+ # Sort hunks by position (reversed for bottom-up application)
132
+ sorted_hunks = sorted(
133
+ [h for h in self.hunks if h.approved],
134
+ key=lambda h: h.source_start,
135
+ reverse=True
136
+ )
137
+
138
+ for wrapper in sorted_hunks:
139
+ hunk = wrapper.hunk
140
+
141
+ # Calculate indices for replacement
142
+ start_idx = wrapper.source_start - 1
143
+ end_idx = start_idx + wrapper.source_length
144
+
145
+ # Extract new lines using unidiff's line iteration
146
+ new_lines = []
147
+ for line in hunk:
148
+ if line.is_added or line.is_context:
149
+ # Get line value (already includes newline)
150
+ new_lines.append(line.value)
151
+
152
+ # Replace section
153
+ result_lines[start_idx:end_idx] = new_lines
154
+
155
+ return ''.join(result_lines)
44
156
 
45
157
 
46
158
  class MultiFileResponse:
@@ -254,13 +366,25 @@ class MultiFileResponse:
254
366
  base_path.mkdir(parents=True, exist_ok=True)
255
367
 
256
368
  for file_change in self.files:
369
+ # Skip files marked for skipping
370
+ if hasattr(file_change, 'change_type') and file_change.change_type == 'skip':
371
+ continue
372
+
373
+ # Skip empty content for create (marked as rejected)
374
+ if file_change.change_type == 'create' and not file_change.content:
375
+ continue
376
+
257
377
  full_path = base_path / file_change.path
258
378
 
259
379
  if dry_run:
260
380
  if file_change.change_type == 'create':
261
381
  console.print(f"[dim]Would create: {file_change.path}[/dim]")
262
382
  elif file_change.change_type == 'modify':
263
- console.print(f"[dim]Would modify: {file_change.path}[/dim]")
383
+ if file_change.hunks and any(h.approved for h in file_change.hunks):
384
+ approved_count = sum(1 for h in file_change.hunks if h.approved)
385
+ console.print(f"[dim]Would modify: {file_change.path} ({approved_count}/{len(file_change.hunks)} hunks)[/dim]")
386
+ else:
387
+ console.print(f"[dim]Would modify: {file_change.path}[/dim]")
264
388
  elif file_change.change_type == 'delete':
265
389
  console.print(f"[dim]Would delete: {file_change.path}[/dim]")
266
390
  continue
@@ -274,29 +398,60 @@ class MultiFileResponse:
274
398
  # Create parent directories
275
399
  full_path.parent.mkdir(parents=True, exist_ok=True)
276
400
 
277
- # Write file
278
- full_path.write_text(file_change.content)
279
-
280
- if file_change.change_type == 'create':
281
- console.print(f"[green]✓[/green] Created: {file_change.path}")
282
- elif file_change.change_type == 'modify':
283
- console.print(f"[yellow]✓[/yellow] Modified: {file_change.path}")
401
+ # For modify with hunks, apply only approved hunks
402
+ if file_change.change_type == 'modify' and file_change.hunks:
403
+ # Only write if at least one hunk is approved
404
+ if not any(h.approved for h in file_change.hunks):
405
+ console.print(f"[dim]Skipped: {file_change.path} (no hunks approved)[/dim]")
406
+ continue
407
+
408
+ content_to_write = file_change.apply_approved_hunks()
409
+ full_path.write_text(content_to_write)
410
+ approved_count = sum(1 for h in file_change.hunks if h.approved)
411
+ console.print(f"[yellow]✓[/yellow] Modified: {file_change.path} ({approved_count}/{len(file_change.hunks)} hunks)")
412
+ else:
413
+ # Write file normally (no hunks or create operation)
414
+ full_path.write_text(file_change.content)
415
+
416
+ if file_change.change_type == 'create':
417
+ console.print(f"[green]✓[/green] Created: {file_change.path}")
418
+ elif file_change.change_type == 'modify':
419
+ console.print(f"[yellow]✓[/yellow] Modified: {file_change.path}")
284
420
 
285
- def confirm(self, console: Console) -> bool:
421
+ def confirm(self, console: Console, base_path: Optional[Path] = None) -> bool:
286
422
  """Interactive confirmation prompt.
287
423
 
424
+ Args:
425
+ console: Rich console for output
426
+ base_path: Base directory for file operations (needed for edit/save)
427
+
288
428
  Returns True if user confirms, False otherwise.
289
429
  """
290
430
  if not self.files:
291
431
  return False
292
432
 
293
433
  while True:
294
- response = console.input("\n[cyan]Continue?[/cyan] [dim](Y/n/preview/help)[/dim] ").strip().lower()
434
+ response = console.input("\n[cyan]Continue?[/cyan] [dim](Y/n/preview/patch/edit/save/help)[/dim] ").strip().lower()
295
435
 
296
436
  if response in ('y', 'yes', ''):
297
437
  return True
298
438
  elif response in ('n', 'no'):
299
439
  return False
440
+ elif response == 'patch':
441
+ # Use hunk-by-hunk confirmation
442
+ return self.confirm_with_hunks(console)
443
+ elif response == 'edit':
444
+ # Open files in editor for manual editing
445
+ if base_path:
446
+ self._edit_files(console, base_path)
447
+ else:
448
+ console.print("[red]Edit not available (no base path provided)[/red]")
449
+ elif response == 'save':
450
+ # Save to custom location
451
+ if base_path:
452
+ return self._save_to_location(console, base_path)
453
+ else:
454
+ console.print("[red]Save not available (no base path provided)[/red]")
300
455
  elif response == 'preview':
301
456
  # Show individual file contents
302
457
  for i, file_change in enumerate(self.files, 1):
@@ -315,13 +470,242 @@ class MultiFileResponse:
315
470
  elif response == 'help':
316
471
  console.print("""
317
472
  [bold]Options:[/bold]
318
- y, yes - Proceed with changes
319
- n, no - Cancel
473
+ y, yes - Proceed with all changes
474
+ n, no - Cancel all changes
475
+ patch - Review changes hunk-by-hunk (like git add -p)
476
+ edit - Open files in $EDITOR before applying
477
+ save - Save to custom location/filename
320
478
  preview - Show file contents/diffs
321
479
  help - Show this help
322
480
  """)
323
481
  else:
324
482
  console.print("[red]Invalid response. Type 'help' for options.[/red]")
483
+
484
+ def confirm_with_hunks(self, console: Console) -> bool:
485
+ """Interactive hunk-by-hunk confirmation (like git add -p).
486
+
487
+ Returns True if at least some changes approved, False if all cancelled.
488
+ """
489
+ if not self.files:
490
+ return False
491
+
492
+ # Parse hunks for all modify operations
493
+ for file_change in self.files:
494
+ if file_change.change_type == 'modify':
495
+ file_change.parse_hunks()
496
+
497
+ has_any_approval = False
498
+
499
+ for file_change in self.files:
500
+ console.print(f"\n[bold cyan]File:[/bold cyan] {file_change.path}")
501
+
502
+ if file_change.change_type == 'create':
503
+ console.print(f"[green]Create new file ({file_change.line_count} lines)[/green]")
504
+ response = self._ask_file_action(console, "create")
505
+ if response == 'y':
506
+ # Mark as approved (keep as-is)
507
+ has_any_approval = True
508
+ elif response == 'n':
509
+ # Remove from files list
510
+ file_change.content = '' # Mark for skip
511
+ elif response == 'q':
512
+ return has_any_approval
513
+
514
+ elif file_change.change_type == 'delete':
515
+ console.print("[red]Delete file[/red]")
516
+ response = self._ask_file_action(console, "delete")
517
+ if response == 'y':
518
+ has_any_approval = True
519
+ elif response == 'n':
520
+ file_change.change_type = 'skip' # Mark for skip
521
+ elif response == 'q':
522
+ return has_any_approval
523
+
524
+ elif file_change.change_type == 'modify':
525
+ if not file_change.hunks:
526
+ console.print("[yellow]No hunks to review[/yellow]")
527
+ continue
528
+
529
+ console.print(f"[yellow]Modify file ({len(file_change.hunks)} hunk(s))[/yellow]")
530
+
531
+ for hunk_idx, hunk in enumerate(file_change.hunks, 1):
532
+ console.print(f"\n[bold]Hunk {hunk_idx}/{len(file_change.hunks)}:[/bold]")
533
+
534
+ # Show hunk with syntax highlighting
535
+ hunk_text = str(hunk)
536
+ console.print(Panel(
537
+ Syntax(hunk_text, "diff", theme="monokai", line_numbers=False),
538
+ border_style="yellow",
539
+ title=f"[bold]{file_change.path}[/bold]"
540
+ ))
541
+
542
+ while True:
543
+ response = console.input(
544
+ "[cyan]Apply this hunk?[/cyan] [dim](y/n/s=skip file/q=quit/help)[/dim] "
545
+ ).strip().lower()
546
+
547
+ if response in ('y', 'yes', ''):
548
+ hunk.approved = True
549
+ has_any_approval = True
550
+ break
551
+ elif response in ('n', 'no'):
552
+ hunk.approved = False
553
+ break
554
+ elif response in ('s', 'skip'):
555
+ # Skip remaining hunks in this file
556
+ break
557
+ elif response in ('q', 'quit'):
558
+ return has_any_approval
559
+ elif response == 'help':
560
+ console.print("""
561
+ [bold]Hunk Options:[/bold]
562
+ y, yes - Apply this hunk
563
+ n, no - Skip this hunk
564
+ s, skip - Skip remaining hunks in this file
565
+ q, quit - Quit and apply approved hunks so far
566
+ help - Show this help
567
+ """)
568
+ else:
569
+ console.print("[red]Invalid response. Type 'help' for options.[/red]")
570
+
571
+ if response in ('s', 'skip'):
572
+ break
573
+
574
+ return has_any_approval
575
+
576
+ def _ask_file_action(self, console: Console, action: str) -> str:
577
+ """Ask for confirmation on file-level action.
578
+
579
+ Returns: 'y' (yes), 'n' (no), 's' (skip), 'q' (quit)
580
+ """
581
+ while True:
582
+ response = console.input(
583
+ f"[cyan]{action.capitalize()} this file?[/cyan] [dim](y/n/s=skip/q=quit)[/dim] "
584
+ ).strip().lower()
585
+
586
+ if response in ('y', 'yes', 'n', 'no', 's', 'skip', 'q', 'quit', ''):
587
+ if response == '':
588
+ return 'y'
589
+ if response in ('skip',):
590
+ return 's'
591
+ if response in ('quit',):
592
+ return 'q'
593
+ return response[0] # Return first character
594
+ else:
595
+ console.print("[red]Invalid response. Use y/n/s/q[/red]")
596
+
597
+ def _edit_files(self, console: Console, base_path: Path) -> None:
598
+ """Open files in editor for manual editing before applying.
599
+
600
+ Args:
601
+ console: Rich console for output
602
+ base_path: Base directory for file operations
603
+ """
604
+ import os
605
+ import subprocess
606
+ import tempfile
607
+
608
+ editor = os.environ.get('EDITOR', 'vi')
609
+
610
+ console.print(f"\n[cyan]Opening files in {editor}...[/cyan]")
611
+ console.print("[dim]Save and close editor to continue[/dim]\n")
612
+
613
+ for file_change in self.files:
614
+ if file_change.change_type == 'delete':
615
+ console.print(f"[dim]Skipping delete operation: {file_change.path}[/dim]")
616
+ continue
617
+
618
+ # Write content to temp file
619
+ with tempfile.NamedTemporaryFile(mode='w', suffix=f'_{Path(file_change.path).name}', delete=False) as tf:
620
+ tf.write(file_change.content)
621
+ temp_path = tf.name
622
+
623
+ try:
624
+ console.print(f"[bold]Editing:[/bold] {file_change.path}")
625
+
626
+ # Open in editor
627
+ result = subprocess.run([editor, temp_path])
628
+
629
+ if result.returncode == 0:
630
+ # Read edited content
631
+ with open(temp_path, 'r') as f:
632
+ edited_content = f.read()
633
+
634
+ # Update file change with edited content
635
+ file_change.content = edited_content
636
+ console.print(f"[green]✓[/green] Updated: {file_change.path}")
637
+ else:
638
+ console.print(f"[yellow]Editor exited with error, keeping original content[/yellow]")
639
+ finally:
640
+ # Clean up temp file
641
+ try:
642
+ os.unlink(temp_path)
643
+ except:
644
+ pass
645
+
646
+ console.print("\n[green]Editing complete![/green]")
647
+
648
+ def _save_to_location(self, console: Console, base_path: Path) -> bool:
649
+ """Save files to custom location.
650
+
651
+ Args:
652
+ console: Rich console for output
653
+ base_path: Base directory for file operations
654
+
655
+ Returns: True if saved successfully, False otherwise
656
+ """
657
+ console.print("\n[cyan]Save options:[/cyan]")
658
+
659
+ if len(self.files) == 1:
660
+ # Single file - ask for filename
661
+ console.print("[dim]Enter filename (or path) to save to:[/dim]")
662
+ filename = console.input("[cyan]Filename:[/cyan] ").strip()
663
+
664
+ if not filename:
665
+ console.print("[yellow]Cancelled[/yellow]")
666
+ return False
667
+
668
+ save_path = Path(filename)
669
+ if not save_path.is_absolute():
670
+ save_path = base_path / save_path
671
+
672
+ # Create parent directories
673
+ save_path.parent.mkdir(parents=True, exist_ok=True)
674
+
675
+ # Write file
676
+ file_change = self.files[0]
677
+ save_path.write_text(file_change.content)
678
+ console.print(f"[green]✓[/green] Saved to: {save_path}")
679
+ return False # Don't continue with original write
680
+ else:
681
+ # Multiple files - ask for directory
682
+ console.print(f"[dim]Enter directory to save {len(self.files)} file(s) to:[/dim]")
683
+ dirname = console.input("[cyan]Directory:[/cyan] ").strip()
684
+
685
+ if not dirname:
686
+ console.print("[yellow]Cancelled[/yellow]")
687
+ return False
688
+
689
+ save_dir = Path(dirname)
690
+ if not save_dir.is_absolute():
691
+ save_dir = base_path / save_dir
692
+
693
+ # Create directory
694
+ save_dir.mkdir(parents=True, exist_ok=True)
695
+
696
+ # Write all files
697
+ for file_change in self.files:
698
+ if file_change.change_type == 'delete':
699
+ console.print(f"[dim]Skipping delete operation: {file_change.path}[/dim]")
700
+ continue
701
+
702
+ file_path = save_dir / file_change.path
703
+ file_path.parent.mkdir(parents=True, exist_ok=True)
704
+ file_path.write_text(file_change.content)
705
+ console.print(f"[green]✓[/green] Saved: {file_path}")
706
+
707
+ console.print(f"\n[green]All files saved to: {save_dir}[/green]")
708
+ return False # Don't continue with original write
325
709
 
326
710
 
327
711
  def extract_code_blocks(text: str) -> List[Tuple[str, str, str]]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-dev-cli
3
- Version: 0.12.1
3
+ Version: 0.13.3
4
4
  Summary: A powerful CLI tool for developers using Claude AI with multi-API routing, test generation, code review, and usage tracking
5
5
  Author-email: Julio <thinmanj@users.noreply.github.com>
6
6
  License: MIT
@@ -29,6 +29,7 @@ Requires-Dist: pydantic>=2.0.0
29
29
  Requires-Dist: keyring>=24.0.0
30
30
  Requires-Dist: cryptography>=41.0.0
31
31
  Requires-Dist: pyyaml>=6.0.0
32
+ Requires-Dist: unidiff>=0.7.0
32
33
  Provides-Extra: toon
33
34
  Requires-Dist: toon-format>=0.1.0; extra == "toon"
34
35
  Provides-Extra: plugins
@@ -281,22 +282,28 @@ cdc review -m powerful complex_file.py # More thorough review
281
282
  cdc generate tests -m smart mymodule.py # Balanced approach
282
283
  ```
283
284
 
284
- ### 3. Code Generation Commands (NEW in v0.12.0)
285
+ ### 3. Code Generation Commands (NEW in v0.12.0, enhanced v0.13.0)
285
286
 
286
287
  ```bash
287
- # Generate code from specification
288
+ # Generate code from specification (single file)
288
289
  cdc generate code --description "REST API client for weather data" -o client.py
289
290
  cdc generate code --file spec.md -o implementation.go
290
291
  cdc generate code --pdf requirements.pdf -o app.js
291
292
  cdc generate code --url https://example.com/api-spec -o service.py
292
293
 
294
+ # Generate multi-file projects (NEW in v0.13.0)
295
+ cdc generate code --description "FastAPI REST API with auth" -o my-api/
296
+ cdc generate code --file spec.md -o project/ --dry-run # Preview first
297
+ cdc generate code --file spec.md -o project/ --yes # No confirmation
298
+ # Creates complete directory structure with multiple files
299
+
293
300
  # Generate code with interactive refinement
294
301
  cdc generate code --description "Database ORM" -o orm.py --interactive
295
302
 
296
303
  # Generate code with project context
297
304
  cdc generate code --file spec.md -o service.py --auto-context
298
305
 
299
- # Add features to existing project
306
+ # Add features to existing project (NEW: multi-file output in v0.13.0)
300
307
  cdc generate feature --description "Add user authentication with JWT" src/
301
308
  cdc generate feature --file feature-spec.md
302
309
  cdc generate feature --pdf product-requirements.pdf --preview
@@ -304,11 +311,113 @@ cdc generate feature --url https://example.com/feature-spec
304
311
 
305
312
  # Preview feature changes before applying
306
313
  cdc generate feature --description "Add caching layer" src/ --preview
314
+ cdc generate feature --description "Add REST API" --dry-run # Preview changes
315
+ cdc generate feature --file spec.md --yes # Apply without confirmation
307
316
 
308
317
  # Interactive feature implementation
309
318
  cdc generate feature --description "Add logging" src/ --interactive
319
+
320
+ # Hunk-by-hunk approval (like git add -p) - NEW in v0.13.1
321
+ cdc generate feature -f spec.md
322
+ # At confirmation prompt, type 'patch' to review changes hunk-by-hunk
323
+ # Options: y (yes), n (no), s (skip file), q (quit), help
324
+ ```
325
+
326
+ ### 3.1 Interactive Diff Approval (v0.13.1+)
327
+
328
+ When applying file modifications, you can review and approve changes hunk-by-hunk, similar to `git add -p`:
329
+
330
+ ```bash
331
+ # After generating feature/refactor changes:
332
+ cdc generate feature -f spec.md
333
+
334
+ # At the confirmation prompt:
335
+ Continue? (Y/n/preview/patch/help) patch
336
+
337
+ # For each file:
338
+ File: src/main.py
339
+ Modify file (3 hunk(s))
340
+
341
+ Hunk 1/3:
342
+ [Shows diff with syntax highlighting]
343
+ @@ -10,3 +10,5 @@
344
+ def main():
345
+ - print("old")
346
+ + print("new")
347
+ + logging.info("Started")
348
+
349
+ Apply this hunk? (y/n/s=skip file/q=quit/help) y # Approve this hunk
350
+
351
+ Hunk 2/3:
352
+ [Shows next diff]
353
+ Apply this hunk? (y/n/s=skip file/q=quit/help) n # Skip this hunk
354
+
355
+ Hunk 3/3:
356
+ [Shows final diff]
357
+ Apply this hunk? (y/n/s=skip file/q=quit/help) s # Skip remaining in file
358
+
359
+ # File operations options:
360
+ Create this file? (y/n/s=skip/q=quit) y # For new files
361
+ Delete this file? (y/n/s=skip/q=quit) n # For file deletions
362
+ ```
363
+
364
+ **Options:**
365
+ - `y, yes` - Apply this hunk/file
366
+ - `n, no` - Skip this hunk (keeps original)
367
+ - `s, skip` - Skip remaining hunks in current file
368
+ - `q, quit` - Stop reviewing and apply approved changes so far
369
+ - `edit` - Open files in $EDITOR before applying
370
+ - `save` - Save to custom location
371
+ - `help` - Show help message
372
+
373
+ **Benefits:**
374
+ - Fine-grained control over changes
375
+ - Keep original code for some hunks while applying others
376
+ - Syntax-highlighted diffs for easy review
377
+ - Edit files before applying for manual tweaks
378
+ - Save to custom location without -o flag
379
+ - Safe: only approved hunks are written
380
+
381
+ ### 3.2 Edit and Save Options (v0.13.2+)
382
+
383
+ Before applying changes, you can edit files or save to custom locations:
384
+
385
+ #### Edit in $EDITOR
386
+ ```bash
387
+ cdc generate feature -f spec.md
388
+
389
+ # At confirmation:
390
+ Continue? (Y/n/preview/patch/edit/save/help) edit
391
+
392
+ # Opens each file in your $EDITOR (vi, nano, code, etc.)
393
+ # Make manual adjustments, save and close
394
+ # Changes are applied after editing
395
+ ```
396
+
397
+ #### Save to Custom Location
398
+ ```bash
399
+ cdc generate code -d "REST API" -o /tmp/output
400
+
401
+ # At confirmation:
402
+ Continue? (Y/n/preview/patch/edit/save/help) save
403
+
404
+ # Single file:
405
+ Filename: my-custom-name.py # Save to custom filename
406
+
407
+ # Multiple files:
408
+ Directory: /path/to/output/ # Save entire project elsewhere
310
409
  ```
311
410
 
411
+ **Use Cases:**
412
+ - **Edit**: Make manual tweaks before applying (fix formatting, adjust logic)
413
+ - **Save**: Try changes elsewhere before applying to project
414
+ - **Edit + Preview**: Review, edit, then apply with confidence
415
+ - **Save for later**: Generate code, save it, review offline, apply manually
416
+
417
+ **Environment Variables:**
418
+ - `$EDITOR`: Your preferred editor (e.g., `export EDITOR=nano`)
419
+ - Defaults to `vi` if `$EDITOR` not set
420
+
312
421
  ### 4. Developer Commands
313
422
 
314
423
  ```bash
@@ -363,11 +472,16 @@ cdc generate docs mymodule.py --auto-context
363
472
  # Refactor (single file)
364
473
  cdc refactor legacy_code.py
365
474
 
366
- # Refactor multiple files (NEW in v0.11.0)
475
+ # Refactor multiple files (NEW in v0.11.0, enhanced v0.13.0)
367
476
  cdc refactor file1.py file2.py file3.py
368
477
  cdc refactor src/
369
478
  cdc refactor # Auto-detect git changes
370
479
 
480
+ # Multi-file refactoring with preview (NEW in v0.13.0)
481
+ cdc refactor src/ --dry-run # Preview changes
482
+ cdc refactor src/ --yes # Apply without confirmation
483
+ cdc refactor src/ --preview # Review before applying
484
+
371
485
  # Refactor with context (includes related files)
372
486
  cdc refactor legacy_code.py --auto-context
373
487
 
@@ -1,12 +1,12 @@
1
- claude_dev_cli/__init__.py,sha256=m4e1TLJ_BCtWjtXr5lJcRupHDnyE30CEI2yqfFmbcWo,470
2
- claude_dev_cli/cli.py,sha256=GDURdznUC9Dy_B9x8MS-9J3JrMbwiVOofSWHalmsRaU,91797
1
+ claude_dev_cli/__init__.py,sha256=ouhf-Bq_KvlUTS5g2fBYhDTVnw6j58pIS02Nrr91GbE,470
2
+ claude_dev_cli/cli.py,sha256=q_vAEQrY52J5kQJCfm-jqD0ygRDnCc2eR_7NCVz1fJo,100447
3
3
  claude_dev_cli/commands.py,sha256=RKGx2rv56PM6eErvA2uoQ20hY8babuI5jav8nCUyUOk,3964
4
4
  claude_dev_cli/config.py,sha256=ZnPvzwlXsoY9YhqTl4S__fwY1MzJXKIaYK0nIIelNXk,19978
5
5
  claude_dev_cli/context.py,sha256=1TlLzpREFZDEIuU7RAtlkjxARKWZpnxHHvK283sUAZE,26714
6
6
  claude_dev_cli/core.py,sha256=4tKBgPQzvhM-jtlHaIy2K54vc2yIb4ycNDPLpoIoqN0,6621
7
7
  claude_dev_cli/history.py,sha256=26EjNW68JuFQJhUp1j8UdB19S-eYz3eqevkpCOATwP0,10510
8
8
  claude_dev_cli/input_sources.py,sha256=pFX5pU8uAUW_iujYdV3z1c_6F0KbKTWMNG0ChvKbxC8,7115
9
- claude_dev_cli/multi_file_handler.py,sha256=3Rgy9NetKvd4tFhGyeY-44CYApnNShKAEErWOOARrJw,13331
9
+ claude_dev_cli/multi_file_handler.py,sha256=QU0M7X8g-5e1A6UwVISJNmQqSZ9GUGkeBlMe3d0vb-M,29364
10
10
  claude_dev_cli/path_utils.py,sha256=FFwweSkXe9OiG2Dej_UDKcY8-ZCYjL89ow6c7LZGe80,5564
11
11
  claude_dev_cli/secure_storage.py,sha256=KcZuQMLTbQpMAi2Cyh-_JkNcK9vHzAITOgjTcM9sr98,8161
12
12
  claude_dev_cli/template_manager.py,sha256=wtcrNuxFoJLJIPmIxUzrPKrE8kUvdqEd53EnG3jARhg,9277
@@ -20,9 +20,9 @@ claude_dev_cli/plugins/base.py,sha256=H4HQet1I-a3WLCfE9F06Lp8NuFvVoIlou7sIgyJFK-
20
20
  claude_dev_cli/plugins/diff_editor/__init__.py,sha256=gqR5S2TyIVuq-sK107fegsutQ7Z-sgAIEbtc71FhXIM,101
21
21
  claude_dev_cli/plugins/diff_editor/plugin.py,sha256=M1bUoqpasD3ZNQo36Fu_8g92uySPZyG_ujMbj5UplsU,3073
22
22
  claude_dev_cli/plugins/diff_editor/viewer.py,sha256=1IOXIKw_01ppJx5C1dQt9Kr6U1TdAHT8_iUT5r_q0NM,17169
23
- claude_dev_cli-0.12.1.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
24
- claude_dev_cli-0.12.1.dist-info/METADATA,sha256=HGhP9cVZEsXsOH9hhtZXRhjfqWrQvsdlxUcWAvzEZgU,24018
25
- claude_dev_cli-0.12.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
26
- claude_dev_cli-0.12.1.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
27
- claude_dev_cli-0.12.1.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
28
- claude_dev_cli-0.12.1.dist-info/RECORD,,
23
+ claude_dev_cli-0.13.3.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
24
+ claude_dev_cli-0.13.3.dist-info/METADATA,sha256=JmMfGjBrcLRx_mvHHLPJ5kUo9Qp1evnEo_N0OVFWvZ8,27704
25
+ claude_dev_cli-0.13.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
26
+ claude_dev_cli-0.13.3.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
27
+ claude_dev_cli-0.13.3.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
28
+ claude_dev_cli-0.13.3.dist-info/RECORD,,