Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion codeflash/api/cfapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,11 @@ def make_cfapi_request(


@lru_cache(maxsize=1)
def get_user_id(api_key: Optional[str] = None) -> Optional[str]:
def get_user_id(api_key: Optional[str] = None, *, suppress_errors: bool = False) -> Optional[str]:
"""Retrieve the user's userid by making a request to the /cfapi/cli-get-user endpoint.

:param api_key: The API key to use. If None, uses get_codeflash_api_key().
:param suppress_errors: If True, avoid exiting on auth/version errors and return None instead.
:return: The userid or None if the request fails.
"""
lsp_enabled = is_LSP_enabled()
Expand All @@ -129,6 +130,9 @@ def get_user_id(api_key: Optional[str] = None) -> Optional[str]:
if min_version and version.parse(min_version) > version.parse(__version__):
msg = "Your Codeflash CLI version is outdated. Please update to the latest version using `pip install --upgrade codeflash`."
console.print(f"[bold red]{msg}[/bold red]")
if suppress_errors:
logger.debug(msg)
return None
if lsp_enabled:
logger.debug(msg)
return f"Error: {msg}"
Expand All @@ -140,6 +144,9 @@ def get_user_id(api_key: Optional[str] = None) -> Optional[str]:

if response.status_code == 403:
error_title = "Invalid Codeflash API key. The API key you provided is not valid."
if suppress_errors:
logger.debug(error_title)
return None
if lsp_enabled:
return f"Error: {error_title}"
msg = (
Expand Down
8 changes: 6 additions & 2 deletions codeflash/cli_cmds/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,15 @@ def _handle_show_config() -> None:
from codeflash.code_utils.config_parser import parse_config_file

config, config_file_path = parse_config_file()
status = "Saved config"
is_file_backed_config = config_file_path.is_file()
status = "Saved config" if is_file_backed_config else "Auto-detected (zero-config)"

console.print()
console.print(f"[bold]Codeflash Configuration[/bold] ({status})")
console.print(f"[dim]Config file: {config_file_path}[/dim]")
if is_file_backed_config:
console.print(f"[dim]Config file: {config_file_path}[/dim]")
else:
console.print(f"[dim]Config source: {config_file_path}[/dim]")
console.print()

table = Table(show_header=True, header_style="bold cyan")
Expand Down
38 changes: 25 additions & 13 deletions codeflash/cli_cmds/cmd_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
from argparse import Namespace


def init_codeflash() -> None:
def init_codeflash(*, skip_confirm: bool = False, skip_api_key: bool = False) -> None:
try:
welcome_panel = Panel(
Text(
Expand All @@ -63,34 +63,46 @@ def init_codeflash() -> None:
project_language = detect_project_language()

if project_language == ProjectLanguage.GO:
init_go_project()
init_go_project(skip_confirm=skip_confirm, skip_api_key=skip_api_key)
return

if project_language == ProjectLanguage.JAVA:
init_java_project()
init_java_project(skip_confirm=skip_confirm, skip_api_key=skip_api_key)
return

if project_language in (ProjectLanguage.JAVASCRIPT, ProjectLanguage.TYPESCRIPT):
init_js_project(project_language)
init_js_project(project_language, skip_confirm=skip_confirm, skip_api_key=skip_api_key)
return

# Python project flow
did_add_new_key = prompt_api_key()

should_modify, config = should_modify_pyproject_toml()
did_add_new_key = False if skip_api_key else prompt_api_key()
git_remote = "origin"

should_modify, config = should_modify_pyproject_toml(skip_confirm=skip_confirm)
git_remote = config.get("git_remote", "origin") if config else "origin"

if should_modify:
setup_info: CLISetupInfo = collect_setup_info()
git_remote = setup_info.git_remote
configured = configure_pyproject_toml(setup_info)
if not configured:
apologize_and_exit()
if skip_confirm:
from codeflash.setup import detect_project, write_config

detected = detect_project()
configured, message = write_config(detected)
if configured:
click.echo(message)
click.echo()
else:
click.echo(message)
apologize_and_exit()
else:
setup_info = collect_setup_info()
git_remote = setup_info.git_remote
configured = configure_pyproject_toml(setup_info)
if not configured:
apologize_and_exit()

install_github_app(git_remote)

install_github_actions(override_formatter_check=True)
install_github_actions(override_formatter_check=True, skip_confirm=skip_confirm)

install_vscode_extension()

Expand Down
35 changes: 35 additions & 0 deletions codeflash/cli_cmds/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,43 @@

DEBUG_MODE = logging.getLogger().getEffectiveLevel() == logging.DEBUG


def _configure_stdio_for_unicode_safety() -> None:
"""Avoid hard failures when the active console encoding can't represent Unicode."""
for stream in (sys.stdout, sys.stderr):
reconfigure = getattr(stream, "reconfigure", None)
if callable(reconfigure):
with contextlib.suppress(OSError, ValueError):
reconfigure(errors="replace")


def _can_encode(text: str) -> bool:
encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
try:
text.encode(encoding)
except UnicodeEncodeError:
return False
return True


_configure_stdio_for_unicode_safety()

console = Console(highlighter=NullHighlighter())

_original_console_rule = console.rule


def _safe_console_rule(title: str = "", *args: object, **kwargs: object) -> None:
if "characters" not in kwargs and not _can_encode("─"):
kwargs["characters"] = "-"
if title and not _can_encode(title):
encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
title = title.encode(encoding, errors="replace").decode(encoding, errors="replace")
_original_console_rule(title, *args, **kwargs)


console.rule = _safe_console_rule # type: ignore[method-assign]

if is_LSP_enabled() or is_subagent_mode():
console.quiet = True

Expand Down
95 changes: 73 additions & 22 deletions codeflash/cli_cmds/github_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class DependencyManager(Enum):
UNKNOWN = auto()


def install_github_actions(override_formatter_check: bool = False) -> None:
def install_github_actions(override_formatter_check: bool = False, *, skip_confirm: bool = False) -> None:
try:
config, _config_file_path = parse_config_file(override_formatter_check=override_formatter_check)

Expand Down Expand Up @@ -100,12 +100,15 @@ def install_github_actions(override_formatter_check: bool = False) -> None:
console.print(benchmark_panel)
console.print()

benchmark_questions = [
inquirer.Confirm("benchmark_mode", message="Run GitHub Actions in benchmark mode?", default=True)
]
if skip_confirm:
benchmark_mode = True
else:
benchmark_questions = [
inquirer.Confirm("benchmark_mode", message="Run GitHub Actions in benchmark mode?", default=True)
]

benchmark_answers = inquirer.prompt(benchmark_questions, theme=CodeflashTheme())
benchmark_mode = benchmark_answers["benchmark_mode"] if benchmark_answers else False
benchmark_answers = inquirer.prompt(benchmark_questions, theme=CodeflashTheme())
benchmark_mode = benchmark_answers["benchmark_mode"] if benchmark_answers else False

# Show prompt only if workflow doesn't exist locally
actions_panel = Panel(
Expand All @@ -121,26 +124,28 @@ def install_github_actions(override_formatter_check: bool = False) -> None:
console.print(actions_panel)
console.print()

creation_questions = [
inquirer.Confirm(
"confirm_creation",
message="Set up GitHub Actions for continuous optimization? We'll open a pull request with the workflow file.",
default=True,
)
]
if skip_confirm:
confirm_creation = True
else:
creation_questions = [
inquirer.Confirm(
"confirm_creation",
message="Set up GitHub Actions for continuous optimization? We'll open a pull request with the workflow file.",
default=True,
)
]

creation_answers = inquirer.prompt(creation_questions, theme=CodeflashTheme())
confirm_creation = bool(creation_answers and creation_answers["confirm_creation"])

creation_answers = inquirer.prompt(creation_questions, theme=CodeflashTheme())
if not creation_answers or not creation_answers["confirm_creation"]:
if not confirm_creation:
skip_panel = Panel(
Text("⏩️ Skipping GitHub Actions setup.", style="yellow"), title="⏩️ Skipped", border_style="yellow"
)
console.print(skip_panel)
ph("cli-github-workflow-skipped")
return
ph(
"cli-github-optimization-confirm-workflow-creation",
{"confirm_creation": creation_answers["confirm_creation"]},
)
ph("cli-github-optimization-confirm-workflow-creation", {"confirm_creation": confirm_creation})

# Generate workflow content AFTER user confirmation
logger.info("[github_workflow.py:install_github_actions] User confirmed, generating workflow content...")
Expand Down Expand Up @@ -423,6 +428,11 @@ def install_github_actions(override_formatter_check: bool = False) -> None:
f"🚀 Codeflash is now configured to automatically optimize new Github PRs!{LF}"
)

if skip_confirm:
click.echo("Add your CODEFLASH_API_KEY as a GitHub secret before running this workflow.")
ph("cli-github-workflow-created")
return

# Show GitHub secrets setup panel (needed in both cases - PR created via API or local file)
try:
existing_api_key = get_codeflash_api_key()
Expand Down Expand Up @@ -555,8 +565,11 @@ def get_github_action_working_directory(toml_path: Path, git_root: Path) -> str:
def detect_project_language_for_workflow(project_root: Path) -> str:
"""Detect the primary language of the project for workflow generation.

Returns: 'python', 'javascript', 'typescript', or 'java'
Returns: 'python', 'javascript', 'typescript', 'java', or 'go'
"""
if (project_root / "go.mod").exists():
return "go"

# Check for Java build tools first (pom.xml or build.gradle)
if (
(project_root / "pom.xml").exists()
Expand Down Expand Up @@ -693,9 +706,9 @@ def generate_dynamic_workflow_content(
# Detect project language
project_language = detect_project_language_for_workflow(Path.cwd())

# For JavaScript/TypeScript and Java projects, use static template customization
# For JavaScript/TypeScript, Java, and Go projects, use static template customization
# (AI-generated steps are currently Python-only)
if project_language in ("javascript", "typescript", "java"):
if project_language in ("javascript", "typescript", "java", "go"):
return customize_codeflash_yaml_content(optimize_yml_content, config, git_root, benchmark_mode)

# Python project - try AI-generated steps
Expand Down Expand Up @@ -824,6 +837,9 @@ def customize_codeflash_yaml_content(
if project_language in ("javascript", "typescript"):
return _customize_js_workflow_content(optimize_yml_content, git_root, benchmark_mode)

if project_language == "go":
return _customize_go_workflow_content(optimize_yml_content, git_root, benchmark_mode)

# Python project (default)
return _customize_python_workflow_content(optimize_yml_content, git_root, benchmark_mode)

Expand Down Expand Up @@ -948,3 +964,38 @@ def _customize_java_workflow_content(optimize_yml_content: str, git_root: Path)
# Install dependencies command
install_deps = get_java_dependency_installation_commands(build_tool)
return optimize_yml_content.replace("{{ install_dependencies_command }}", install_deps)


def _customize_go_workflow_content(optimize_yml_content: str, git_root: Path, benchmark_mode: bool = False) -> str:
"""Customize workflow content for Go projects."""
from codeflash.cli_cmds.init_go import get_go_dependency_installation_commands, get_go_runtime_setup_steps

project_root = Path.cwd()

if project_root == git_root:
working_dir = ""
else:
rel_path = str(project_root.relative_to(git_root))
working_dir = f"""defaults:
run:
working-directory: ./{rel_path}"""

optimize_yml_content = optimize_yml_content.replace("Optimize new Python code", "Optimize new Go code")
optimize_yml_content = optimize_yml_content.replace("{{ working_directory }}", working_dir)

python_setup = get_dependency_manager_installation_string(DependencyManager.PIP)
go_setup = get_go_runtime_setup_steps()
setup_runtime = f"""{python_setup}
{go_setup}"""
optimize_yml_content = optimize_yml_content.replace("{{ setup_runtime_environment }}", setup_runtime)

install_deps = f"""|
python -m pip install --upgrade pip
pip install codeflash
{get_go_dependency_installation_commands()}"""
optimize_yml_content = optimize_yml_content.replace("{{ install_dependencies_command }}", install_deps)

codeflash_cmd = "codeflash"
if benchmark_mode:
codeflash_cmd += " --benchmark"
return optimize_yml_content.replace("{{ codeflash_command }}", codeflash_cmd)
28 changes: 23 additions & 5 deletions codeflash/cli_cmds/init_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,17 @@ def enter_api_key_and_save_to_rc() -> None:
os.environ["CODEFLASH_API_KEY"] = api_key


def _skip_github_app_installation(owner: str, repo: str) -> None:
click.echo(
f"Skipping Codeflash GitHub app installation for {owner}/{repo}.{LF}"
"Codeflash setup will continue, but PR creation will stay disabled until you install the app later."
f"{LF}In the meantime you can make local only optimizations by using the '--no-pr' flag with codeflash.{LF}"
)


def install_github_app(git_remote: str) -> None:
from rich.prompt import Confirm

try:
git_repo = git.Repo(search_parent_directories=True)
except git.InvalidGitRepositoryError:
Expand All @@ -167,6 +177,17 @@ def install_github_app(git_remote: str) -> None:

else:
try:
should_install = Confirm.ask(
"Do you want to install the Codeflash GitHub app now? You can skip this and continue setup, "
"but Codeflash won't be able to create PRs until the app is installed.",
default=True,
show_default=True,
console=console,
)
if not should_install:
_skip_github_app_installation(owner, repo)
return

click.prompt(
f"Finally, you'll need to install the Codeflash GitHub app by choosing the repository you want to install Codeflash on.{LF}"
f"I will attempt to open the github app page - https://github.com/apps/codeflash-ai/installations/select_target {LF}"
Expand All @@ -188,11 +209,7 @@ def install_github_app(git_remote: str) -> None:
count = 2
while not is_github_app_installed_on_repo(owner, repo, suppress_errors=True):
if count == 0:
click.echo(
f"❌ It looks like the Codeflash GitHub App is not installed on the repository {owner}/{repo}.{LF}"
f"You won't be able to create PRs with Codeflash until you install the app.{LF}"
f"In the meantime you can make local only optimizations by using the '--no-pr' flag with codeflash.{LF}"
)
_skip_github_app_installation(owner, repo)
break
click.prompt(
f"❌ It looks like the Codeflash GitHub App is not installed on the repository {owner}/{repo}.{LF}"
Expand All @@ -207,3 +224,4 @@ def install_github_app(git_remote: str) -> None:
except (KeyboardInterrupt, EOFError, click.exceptions.Abort):
# leave empty line for the next prompt to be properly rendered
click.echo()
_skip_github_app_installation(owner, repo)
5 changes: 4 additions & 1 deletion codeflash/cli_cmds/init_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def is_valid_pyproject_toml(pyproject_toml_path: Union[str, Path]) -> tuple[bool
return True, config, ""


def should_modify_pyproject_toml() -> tuple[bool, dict[str, Any] | None]:
def should_modify_pyproject_toml(*, skip_confirm: bool = False) -> tuple[bool, dict[str, Any] | None]:
"""Check if the current directory contains a valid pyproject.toml file with codeflash config.

If it does, ask the user if they want to re-configure it.
Expand All @@ -160,6 +160,9 @@ def should_modify_pyproject_toml() -> tuple[bool, dict[str, Any] | None]:
# needs to be re-configured
return True, None

if skip_confirm:
return False, config

return Confirm.ask(
"✅ A valid Codeflash config already exists in this project. Do you want to re-configure it?",
default=False,
Expand Down
Loading
Loading