Skip to content

Commit be02b40

Browse files
Merge branch 'main' into fix/get-language-based-on-formatter
2 parents 9c7fa4f + eb1d27e commit be02b40

31 files changed

Lines changed: 4589 additions & 82 deletions

CLAUDE.md

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,11 @@ codeflash/
5757
└── result/ # Result types and handling
5858
```
5959

60-
### Key Patterns
61-
62-
**Either/Result pattern for errors:**
63-
```python
64-
from codeflash.either import is_successful, Success, Failure
65-
result = some_operation()
66-
if is_successful(result):
67-
value = result.unwrap()
68-
else:
69-
error = result.failure()
70-
```
60+
### Key Rules to follow
7161

72-
**Use libcst, not ast** - Always use `libcst` for code parsing/modification to preserve formatting.
62+
- Use libcst, not ast - For Python, always use `libcst` for code parsing/modification to preserve formatting.
63+
- Code context extraction and replacement tests must always assert for full string equality, no substring matching.
64+
- Any new feature or bug fix that can be tested automatically must have test cases.
7365

7466
## Code Style
7567

codeflash/cli_cmds/cli.py

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,15 @@ def parse_args() -> Namespace:
121121
"--effort", type=str, help="Effort level for optimization", choices=["low", "medium", "high"], default="medium"
122122
)
123123

124+
# Config management flags
125+
parser.add_argument(
126+
"--show-config", action="store_true", help="Show current or auto-detected configuration and exit."
127+
)
128+
parser.add_argument(
129+
"--reset-config", action="store_true", help="Remove codeflash configuration from project config file."
130+
)
131+
parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompts (useful for CI/scripts).")
132+
124133
args, unknown_args = parser.parse_known_args()
125134
sys.argv[:] = [sys.argv[0], *unknown_args]
126135
return process_and_validate_cmd_args(args)
@@ -147,6 +156,16 @@ def process_and_validate_cmd_args(args: Namespace) -> Namespace:
147156
logger.info(f"Codeflash version {version}")
148157
sys.exit()
149158

159+
# Handle --show-config
160+
if getattr(args, "show_config", False):
161+
_handle_show_config()
162+
sys.exit()
163+
164+
# Handle --reset-config
165+
if getattr(args, "reset_config", False):
166+
_handle_reset_config(confirm=not getattr(args, "yes", False))
167+
sys.exit()
168+
150169
if args.command == "vscode-install":
151170
install_vscode_extension()
152171
sys.exit()
@@ -212,11 +231,22 @@ def process_pyproject_config(args: Namespace) -> Namespace:
212231
is_js_ts_project = pyproject_config.get("language") in ("javascript", "typescript")
213232
if args.tests_root is None:
214233
if is_js_ts_project:
215-
# Try common JS test directories, or default to module_root
234+
# Try common JS test directories at project root first
216235
for test_dir in ["test", "tests", "__tests__"]:
217236
if Path(test_dir).is_dir():
218237
args.tests_root = test_dir
219238
break
239+
# If not found at project root, try inside module_root (e.g., src/test, src/__tests__)
240+
if args.tests_root is None and args.module_root:
241+
module_root_path = Path(args.module_root)
242+
for test_dir in ["test", "tests", "__tests__"]:
243+
test_path = module_root_path / test_dir
244+
if test_path.is_dir():
245+
args.tests_root = str(test_path)
246+
break
247+
# Final fallback: default to module_root
248+
# Note: This may cause issues if tests are colocated with source files
249+
# In such cases, the user should explicitly configure testsRoot in package.json
220250
if args.tests_root is None:
221251
args.tests_root = args.module_root
222252
else:
@@ -309,3 +339,91 @@ def handle_optimize_all_arg_parsing(args: Namespace) -> Namespace:
309339
else:
310340
args.all = Path(args.all).resolve()
311341
return args
342+
343+
344+
def _handle_show_config() -> None:
345+
"""Show current or auto-detected Codeflash configuration."""
346+
from rich.table import Table
347+
348+
from codeflash.cli_cmds.console import console
349+
from codeflash.setup.detector import detect_project, has_existing_config
350+
351+
project_root = Path.cwd()
352+
detected = detect_project(project_root)
353+
354+
# Check if config exists or is auto-detected
355+
config_exists = has_existing_config(project_root)
356+
status = "Saved config" if config_exists else "Auto-detected (not saved)"
357+
358+
console.print()
359+
console.print(f"[bold]Codeflash Configuration[/bold] ({status})")
360+
console.print()
361+
362+
table = Table(show_header=True, header_style="bold cyan")
363+
table.add_column("Setting", style="dim")
364+
table.add_column("Value")
365+
366+
table.add_row("Language", detected.language)
367+
table.add_row("Project root", str(detected.project_root))
368+
table.add_row("Module root", str(detected.module_root))
369+
table.add_row("Tests root", str(detected.tests_root) if detected.tests_root else "(not detected)")
370+
table.add_row("Test runner", detected.test_runner or "(not detected)")
371+
table.add_row("Formatter", ", ".join(detected.formatter_cmds) if detected.formatter_cmds else "(not detected)")
372+
table.add_row(
373+
"Ignore paths", ", ".join(str(p) for p in detected.ignore_paths) if detected.ignore_paths else "(none)"
374+
)
375+
table.add_row("Confidence", f"{detected.confidence:.0%}")
376+
377+
console.print(table)
378+
console.print()
379+
380+
if not config_exists:
381+
console.print("[dim]Run [bold]codeflash --file <file>[/bold] to auto-save this config.[/dim]")
382+
383+
384+
def _handle_reset_config(confirm: bool = True) -> None:
385+
"""Remove Codeflash configuration from project config file.
386+
387+
Args:
388+
confirm: If True, prompt for confirmation before removing.
389+
390+
"""
391+
from codeflash.cli_cmds.console import console
392+
from codeflash.setup.config_writer import remove_config
393+
from codeflash.setup.detector import detect_project, has_existing_config
394+
395+
project_root = Path.cwd()
396+
397+
if not has_existing_config(project_root):
398+
console.print("[yellow]No Codeflash configuration found to remove.[/yellow]")
399+
return
400+
401+
detected = detect_project(project_root)
402+
403+
if confirm:
404+
console.print("[bold]This will remove Codeflash configuration from your project.[/bold]")
405+
console.print()
406+
407+
config_file = "pyproject.toml" if detected.language == "python" else "package.json"
408+
console.print(f" Config file: {project_root / config_file}")
409+
console.print()
410+
411+
try:
412+
response = console.input("[bold]Are you sure you want to remove the config? [y/N][/bold] ")
413+
except (EOFError, KeyboardInterrupt):
414+
console.print("\n[yellow]Cancelled.[/yellow]")
415+
return
416+
417+
if response.lower() not in ("y", "yes"):
418+
console.print("[yellow]Cancelled.[/yellow]")
419+
return
420+
421+
success, message = remove_config(project_root, detected.language)
422+
423+
# Escape brackets in message to prevent Rich markup interpretation
424+
escaped_message = message.replace("[", "\\[")
425+
426+
if success:
427+
console.print(f"[green]✓[/green] {escaped_message}")
428+
else:
429+
console.print(f"[red]✗[/red] {escaped_message}")

codeflash/cli_cmds/cmd_init.py

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,33 @@ def check_for_toml_or_setup_file() -> str | None:
607607
curdir = Path.cwd()
608608
pyproject_toml_path = curdir / "pyproject.toml"
609609
setup_py_path = curdir / "setup.py"
610+
package_json_path = curdir / "package.json"
610611
project_name = None
612+
613+
# Check if this might be a JavaScript/TypeScript project that wasn't detected
614+
if package_json_path.exists() and not pyproject_toml_path.exists() and not setup_py_path.exists():
615+
js_redirect_panel = Panel(
616+
Text(
617+
f"📦 I found a package.json in {curdir}.\n\n"
618+
"This looks like a JavaScript/TypeScript project!\n"
619+
"Redirecting to JavaScript setup...",
620+
style="cyan",
621+
),
622+
title="🟨 JavaScript Project Detected",
623+
border_style="bright_yellow",
624+
)
625+
console.print(js_redirect_panel)
626+
console.print()
627+
ph("cli-js-project-redirect")
628+
629+
# Redirect to JS init
630+
from codeflash.cli_cmds.init_javascript import ProjectLanguage, detect_project_language, init_js_project
631+
632+
project_language = detect_project_language()
633+
if project_language in (ProjectLanguage.JAVASCRIPT, ProjectLanguage.TYPESCRIPT):
634+
init_js_project(project_language)
635+
sys.exit(0) # init_js_project handles its own exit, but ensure we don't continue
636+
611637
if pyproject_toml_path.exists():
612638
try:
613639
pyproject_toml_content = pyproject_toml_path.read_text(encoding="utf8")
@@ -617,28 +643,44 @@ def check_for_toml_or_setup_file() -> str | None:
617643
except Exception:
618644
click.echo("✅ I found a pyproject.toml for your project.")
619645
ph("cli-pyproject-toml-found")
646+
elif setup_py_path.exists():
647+
setup_py_content = setup_py_path.read_text(encoding="utf8")
648+
project_name_match = re.search(r"setup\s*\([^)]*?name\s*=\s*['\"](.*?)['\"]", setup_py_content, re.DOTALL)
649+
if project_name_match:
650+
project_name = project_name_match.group(1)
651+
click.echo(f"✅ Found setup.py for your project {project_name}")
652+
ph("cli-setup-py-found-name")
653+
else:
654+
click.echo("✅ Found setup.py.")
655+
ph("cli-setup-py-found")
620656
else:
621-
if setup_py_path.exists():
622-
setup_py_content = setup_py_path.read_text(encoding="utf8")
623-
project_name_match = re.search(r"setup\s*\([^)]*?name\s*=\s*['\"](.*?)['\"]", setup_py_content, re.DOTALL)
624-
if project_name_match:
625-
project_name = project_name_match.group(1)
626-
click.echo(f"✅ Found setup.py for your project {project_name}")
627-
ph("cli-setup-py-found-name")
628-
else:
629-
click.echo("✅ Found setup.py.")
630-
ph("cli-setup-py-found")
631-
toml_info_panel = Panel(
632-
Text(
633-
f"💡 No pyproject.toml found in {curdir}.\n\n"
634-
"This file is essential for Codeflash to store its configuration.\n"
635-
"Please ensure you are running `codeflash init` from your project's root directory.",
636-
style="yellow",
637-
),
638-
title="📋 pyproject.toml Required",
639-
border_style="bright_yellow",
640-
)
641-
console.print(toml_info_panel)
657+
# No Python config files found - show appropriate message
658+
# Check again if this might be a JS project
659+
if package_json_path.exists():
660+
js_hint_panel = Panel(
661+
Text(
662+
f"📦 I found a package.json but no pyproject.toml in {curdir}.\n\n"
663+
"If this is a JavaScript/TypeScript project, please run:\n"
664+
" codeflash init\n\n"
665+
"from the project root directory.",
666+
style="yellow",
667+
),
668+
title="🤔 Mixed Project?",
669+
border_style="bright_yellow",
670+
)
671+
console.print(js_hint_panel)
672+
else:
673+
toml_info_panel = Panel(
674+
Text(
675+
f"💡 No pyproject.toml found in {curdir}.\n\n"
676+
"This file is essential for Codeflash to store its configuration.\n"
677+
"Please ensure you are running `codeflash init` from your project's root directory.",
678+
style="yellow",
679+
),
680+
title="📋 pyproject.toml Required",
681+
border_style="bright_yellow",
682+
)
683+
console.print(toml_info_panel)
642684
console.print()
643685
ph("cli-no-pyproject-toml-or-setup-py")
644686

codeflash/cli_cmds/console.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ def code_print(
129129

130130
spinners = cycle(SPINNER_TYPES)
131131

132+
# Track whether a progress bar is already active to prevent nested Live displays
133+
_progress_bar_active = False
134+
132135

133136
@contextmanager
134137
def progress_bar(
@@ -138,28 +141,38 @@ def progress_bar(
138141
139142
If revert_to_print is True, falls back to printing a single logger.info message
140143
instead of showing a progress bar.
144+
145+
If a progress bar is already active, yields a dummy task ID to avoid Rich's
146+
LiveError from nested Live displays.
141147
"""
148+
global _progress_bar_active
149+
142150
if is_LSP_enabled():
143151
lsp_log(LspTextMessage(text=message, takes_time=True))
144152
yield
145153
return
146154

147-
if revert_to_print:
148-
logger.info(message)
155+
if revert_to_print or _progress_bar_active:
156+
if revert_to_print:
157+
logger.info(message)
149158

150159
# Create a fake task ID since we still need to yield something
151160
yield DummyTask().id
152161
else:
153-
progress = Progress(
154-
SpinnerColumn(next(spinners)),
155-
*Progress.get_default_columns(),
156-
TimeElapsedColumn(),
157-
console=console,
158-
transient=transient,
159-
)
160-
task = progress.add_task(message, total=None)
161-
with progress:
162-
yield task
162+
_progress_bar_active = True
163+
try:
164+
progress = Progress(
165+
SpinnerColumn(next(spinners)),
166+
*Progress.get_default_columns(),
167+
TimeElapsedColumn(),
168+
console=console,
169+
transient=transient,
170+
)
171+
task = progress.add_task(message, total=None)
172+
with progress:
173+
yield task
174+
finally:
175+
_progress_bar_active = False
163176

164177

165178
@contextmanager

codeflash/cli_cmds/init_javascript.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,30 @@ def detect_project_language(project_root: Path | None = None) -> ProjectLanguage
9090
has_package_json = (root / "package.json").exists()
9191
has_tsconfig = (root / "tsconfig.json").exists()
9292

93-
# TypeScript project
93+
# TypeScript project (tsconfig.json is definitive)
9494
if has_tsconfig:
9595
return ProjectLanguage.TYPESCRIPT
9696

97-
# Pure JS project (has package.json but no Python files)
98-
if has_package_json and not has_pyproject and not has_setup_py:
99-
return ProjectLanguage.JAVASCRIPT
97+
# JavaScript project - package.json without Python-specific files takes priority
98+
# Note: If both package.json and pyproject.toml exist, check for typical JS project indicators
99+
if has_package_json:
100+
# If no Python config files, it's definitely JavaScript
101+
if not has_pyproject and not has_setup_py:
102+
return ProjectLanguage.JAVASCRIPT
103+
104+
# If package.json exists with Python files, check for JS-specific indicators
105+
# Common React/Node patterns indicate a JS project
106+
js_indicators = [
107+
(root / "node_modules").exists(),
108+
(root / ".npmrc").exists(),
109+
(root / "yarn.lock").exists(),
110+
(root / "package-lock.json").exists(),
111+
(root / "pnpm-lock.yaml").exists(),
112+
(root / "bun.lockb").exists(),
113+
(root / "bun.lock").exists(),
114+
]
115+
if any(js_indicators):
116+
return ProjectLanguage.JAVASCRIPT
100117

101118
# Python project (default)
102119
return ProjectLanguage.PYTHON

codeflash/code_utils/code_replacer.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,19 @@ def _add_global_declarations_for_language(
659659
# Get names of existing declarations
660660
existing_names = {decl.name for decl in original_declarations}
661661

662+
# Also exclude names that are already imported (to avoid duplicating imported types)
663+
original_imports = analyzer.find_imports(original_source)
664+
for imp in original_imports:
665+
# Add default import name
666+
if imp.default_import:
667+
existing_names.add(imp.default_import)
668+
# Add named imports (use alias if present, otherwise use original name)
669+
for name, alias in imp.named_imports:
670+
existing_names.add(alias if alias else name)
671+
# Add namespace import
672+
if imp.namespace_import:
673+
existing_names.add(imp.namespace_import)
674+
662675
# Find new declarations (names that don't exist in original)
663676
new_declarations = []
664677
seen_sources = set() # Track to avoid duplicates from destructuring

0 commit comments

Comments
 (0)