claude-dev-cli 0.13.0__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 +3 -3
- claude_dev_cli/multi_file_handler.py +400 -16
- {claude_dev_cli-0.13.0.dist-info → claude_dev_cli-0.13.3.dist-info}/METADATA +102 -1
- {claude_dev_cli-0.13.0.dist-info → claude_dev_cli-0.13.3.dist-info}/RECORD +9 -9
- {claude_dev_cli-0.13.0.dist-info → claude_dev_cli-0.13.3.dist-info}/WHEEL +0 -0
- {claude_dev_cli-0.13.0.dist-info → claude_dev_cli-0.13.3.dist-info}/entry_points.txt +0 -0
- {claude_dev_cli-0.13.0.dist-info → claude_dev_cli-0.13.3.dist-info}/licenses/LICENSE +0 -0
- {claude_dev_cli-0.13.0.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
|
@@ -1270,7 +1270,7 @@ Use proper directory structure and include proper error handling, documentation,
|
|
|
1270
1270
|
|
|
1271
1271
|
# Confirm or auto-accept
|
|
1272
1272
|
if not yes and not dry_run:
|
|
1273
|
-
if not multi_file.confirm(console):
|
|
1273
|
+
if not multi_file.confirm(console, output_path):
|
|
1274
1274
|
console.print("[yellow]Cancelled[/yellow]")
|
|
1275
1275
|
return
|
|
1276
1276
|
|
|
@@ -1489,7 +1489,7 @@ def gen_feature(
|
|
|
1489
1489
|
|
|
1490
1490
|
# Confirm or auto-accept
|
|
1491
1491
|
if not yes:
|
|
1492
|
-
if not multi_file.confirm(console):
|
|
1492
|
+
if not multi_file.confirm(console, base_path):
|
|
1493
1493
|
console.print("[yellow]Cancelled[/yellow]")
|
|
1494
1494
|
return
|
|
1495
1495
|
|
|
@@ -1855,7 +1855,7 @@ def refactor(
|
|
|
1855
1855
|
|
|
1856
1856
|
# Confirm or auto-accept
|
|
1857
1857
|
if not yes:
|
|
1858
|
-
if not multi_file.confirm(console):
|
|
1858
|
+
if not multi_file.confirm(console, base_path):
|
|
1859
1859
|
console.print("[yellow]Cancelled[/yellow]")
|
|
1860
1860
|
return
|
|
1861
1861
|
|
|
@@ -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.13.
|
|
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
|
|
@@ -315,8 +316,108 @@ cdc generate feature --file spec.md --yes # Apply without confirmation
|
|
|
315
316
|
|
|
316
317
|
# Interactive feature implementation
|
|
317
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
|
|
318
395
|
```
|
|
319
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
|
|
409
|
+
```
|
|
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
|
+
|
|
320
421
|
### 4. Developer Commands
|
|
321
422
|
|
|
322
423
|
```bash
|
|
@@ -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.13.
|
|
24
|
-
claude_dev_cli-0.13.
|
|
25
|
-
claude_dev_cli-0.13.
|
|
26
|
-
claude_dev_cli-0.13.
|
|
27
|
-
claude_dev_cli-0.13.
|
|
28
|
-
claude_dev_cli-0.13.
|
|
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
|