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.
- claude_dev_cli/__init__.py +1 -1
- claude_dev_cli/cli.py +255 -52
- claude_dev_cli/multi_file_handler.py +400 -16
- {claude_dev_cli-0.12.1.dist-info → claude_dev_cli-0.13.3.dist-info}/METADATA +119 -5
- {claude_dev_cli-0.12.1.dist-info → claude_dev_cli-0.13.3.dist-info}/RECORD +9 -9
- {claude_dev_cli-0.12.1.dist-info → claude_dev_cli-0.13.3.dist-info}/WHEEL +0 -0
- {claude_dev_cli-0.12.1.dist-info → claude_dev_cli-0.13.3.dist-info}/entry_points.txt +0 -0
- {claude_dev_cli-0.12.1.dist-info → claude_dev_cli-0.13.3.dist-info}/licenses/LICENSE +0 -0
- {claude_dev_cli-0.12.1.dist-info → claude_dev_cli-0.13.3.dist-info}/top_level.txt +0 -0
claude_dev_cli/__init__.py
CHANGED
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
|
-
|
|
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
|
|
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
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
1201
|
-
|
|
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
|
|
1297
|
-
prompt += "
|
|
1298
|
-
prompt += "
|
|
1299
|
-
prompt += "
|
|
1300
|
-
prompt += "
|
|
1301
|
-
prompt += "
|
|
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, '
|
|
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() == '
|
|
1344
|
-
|
|
1345
|
-
|
|
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
|
|
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
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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.
|
|
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=
|
|
2
|
-
claude_dev_cli/cli.py,sha256=
|
|
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=
|
|
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.
|
|
24
|
-
claude_dev_cli-0.
|
|
25
|
-
claude_dev_cli-0.
|
|
26
|
-
claude_dev_cli-0.
|
|
27
|
-
claude_dev_cli-0.
|
|
28
|
-
claude_dev_cli-0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|