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.

@@ -9,7 +9,7 @@ Features:
9
9
  - Interactive and single-shot modes
10
10
  """
11
11
 
12
- __version__ = "0.13.0"
12
+ __version__ = "0.13.3"
13
13
  __author__ = "Julio"
14
14
  __license__ = "MIT"
15
15
 
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
- 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.13.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
@@ -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=m1PPg9TBL_opaZdRHkV3d_MDa4IFCPqCFE0KUyBlpVg,470
2
- claude_dev_cli/cli.py,sha256=pIIZv3AT0gCF4faTVSlY_d6F-ya9L3gYoVeUTlq07VY,100412
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.13.0.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
24
- claude_dev_cli-0.13.0.dist-info/METADATA,sha256=vzv4dlBOqJ5Y-tz6OgcafaMq1ponwWgYdVhZIkYD13U,24788
25
- claude_dev_cli-0.13.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
26
- claude_dev_cli-0.13.0.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
27
- claude_dev_cli-0.13.0.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
28
- claude_dev_cli-0.13.0.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,,