Skip to content

Commit 1490afd

Browse files
Fix CLI init and optimize flow regressions
1 parent d7f95a5 commit 1490afd

38 files changed

Lines changed: 936 additions & 162 deletions

codeflash/api/cfapi.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,11 @@ def make_cfapi_request(
102102

103103

104104
@lru_cache(maxsize=1)
105-
def get_user_id(api_key: Optional[str] = None) -> Optional[str]:
105+
def get_user_id(api_key: Optional[str] = None, *, suppress_errors: bool = False) -> Optional[str]:
106106
"""Retrieve the user's userid by making a request to the /cfapi/cli-get-user endpoint.
107107
108108
:param api_key: The API key to use. If None, uses get_codeflash_api_key().
109+
:param suppress_errors: If True, avoid exiting on auth/version errors and return None instead.
109110
:return: The userid or None if the request fails.
110111
"""
111112
lsp_enabled = is_LSP_enabled()
@@ -129,6 +130,9 @@ def get_user_id(api_key: Optional[str] = None) -> Optional[str]:
129130
if min_version and version.parse(min_version) > version.parse(__version__):
130131
msg = "Your Codeflash CLI version is outdated. Please update to the latest version using `pip install --upgrade codeflash`."
131132
console.print(f"[bold red]{msg}[/bold red]")
133+
if suppress_errors:
134+
logger.debug(msg)
135+
return None
132136
if lsp_enabled:
133137
logger.debug(msg)
134138
return f"Error: {msg}"
@@ -140,6 +144,9 @@ def get_user_id(api_key: Optional[str] = None) -> Optional[str]:
140144

141145
if response.status_code == 403:
142146
error_title = "Invalid Codeflash API key. The API key you provided is not valid."
147+
if suppress_errors:
148+
logger.debug(error_title)
149+
return None
143150
if lsp_enabled:
144151
return f"Error: {error_title}"
145152
msg = (

codeflash/cli_cmds/cli.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,11 +289,15 @@ def _handle_show_config() -> None:
289289
from codeflash.code_utils.config_parser import parse_config_file
290290

291291
config, config_file_path = parse_config_file()
292-
status = "Saved config"
292+
is_file_backed_config = config_file_path.is_file()
293+
status = "Saved config" if is_file_backed_config else "Auto-detected (zero-config)"
293294

294295
console.print()
295296
console.print(f"[bold]Codeflash Configuration[/bold] ({status})")
296-
console.print(f"[dim]Config file: {config_file_path}[/dim]")
297+
if is_file_backed_config:
298+
console.print(f"[dim]Config file: {config_file_path}[/dim]")
299+
else:
300+
console.print(f"[dim]Config source: {config_file_path}[/dim]")
297301
console.print()
298302

299303
table = Table(show_header=True, header_style="bold cyan")

codeflash/cli_cmds/cmd_init.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
from argparse import Namespace
4545

4646

47-
def init_codeflash() -> None:
47+
def init_codeflash(*, skip_confirm: bool = False, skip_api_key: bool = False) -> None:
4848
try:
4949
welcome_panel = Panel(
5050
Text(
@@ -63,34 +63,47 @@ def init_codeflash() -> None:
6363
project_language = detect_project_language()
6464

6565
if project_language == ProjectLanguage.GO:
66-
init_go_project()
66+
init_go_project(skip_confirm=skip_confirm, skip_api_key=skip_api_key)
6767
return
6868

6969
if project_language == ProjectLanguage.JAVA:
70-
init_java_project()
70+
init_java_project(skip_confirm=skip_confirm, skip_api_key=skip_api_key)
7171
return
7272

7373
if project_language in (ProjectLanguage.JAVASCRIPT, ProjectLanguage.TYPESCRIPT):
74-
init_js_project(project_language)
74+
init_js_project(project_language, skip_confirm=skip_confirm, skip_api_key=skip_api_key)
7575
return
7676

7777
# Python project flow
78-
did_add_new_key = prompt_api_key()
79-
80-
should_modify, config = should_modify_pyproject_toml()
78+
did_add_new_key = False if skip_api_key else prompt_api_key()
79+
git_remote = "origin"
8180

82-
git_remote = config.get("git_remote", "origin") if config else "origin"
81+
if skip_confirm:
82+
from codeflash.setup import detect_project, write_config
8383

84-
if should_modify:
85-
setup_info: CLISetupInfo = collect_setup_info()
86-
git_remote = setup_info.git_remote
87-
configured = configure_pyproject_toml(setup_info)
88-
if not configured:
84+
detected = detect_project()
85+
configured, message = write_config(detected)
86+
if configured:
87+
click.echo(message)
88+
click.echo()
89+
else:
90+
click.echo(message)
8991
apologize_and_exit()
92+
else:
93+
should_modify, config = should_modify_pyproject_toml()
94+
95+
git_remote = config.get("git_remote", "origin") if config else "origin"
96+
97+
if should_modify:
98+
setup_info = collect_setup_info()
99+
git_remote = setup_info.git_remote
100+
configured = configure_pyproject_toml(setup_info)
101+
if not configured:
102+
apologize_and_exit()
90103

91104
install_github_app(git_remote)
92105

93-
install_github_actions(override_formatter_check=True)
106+
install_github_actions(override_formatter_check=True, skip_confirm=skip_confirm)
94107

95108
install_vscode_extension()
96109

codeflash/cli_cmds/console.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,43 @@
4141

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

44+
45+
def _configure_stdio_for_unicode_safety() -> None:
46+
"""Avoid hard failures when the active console encoding can't represent Unicode."""
47+
for stream in (sys.stdout, sys.stderr):
48+
reconfigure = getattr(stream, "reconfigure", None)
49+
if callable(reconfigure):
50+
with contextlib.suppress(OSError, ValueError):
51+
reconfigure(errors="replace")
52+
53+
54+
def _can_encode(text: str) -> bool:
55+
encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
56+
try:
57+
text.encode(encoding)
58+
except UnicodeEncodeError:
59+
return False
60+
return True
61+
62+
63+
_configure_stdio_for_unicode_safety()
64+
4465
console = Console(highlighter=NullHighlighter())
4566

67+
_original_console_rule = console.rule
68+
69+
70+
def _safe_console_rule(title: str = "", *args: object, **kwargs: object) -> None:
71+
if "characters" not in kwargs and not _can_encode("─"):
72+
kwargs["characters"] = "-"
73+
if title and not _can_encode(title):
74+
encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
75+
title = title.encode(encoding, errors="replace").decode(encoding, errors="replace")
76+
_original_console_rule(title, *args, **kwargs)
77+
78+
79+
console.rule = _safe_console_rule # type: ignore[method-assign]
80+
4681
if is_LSP_enabled() or is_subagent_mode():
4782
console.quiet = True
4883

codeflash/cli_cmds/github_workflow.py

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class DependencyManager(Enum):
3434
UNKNOWN = auto()
3535

3636

37-
def install_github_actions(override_formatter_check: bool = False) -> None:
37+
def install_github_actions(override_formatter_check: bool = False, *, skip_confirm: bool = False) -> None:
3838
try:
3939
config, _config_file_path = parse_config_file(override_formatter_check=override_formatter_check)
4040

@@ -100,12 +100,15 @@ def install_github_actions(override_formatter_check: bool = False) -> None:
100100
console.print(benchmark_panel)
101101
console.print()
102102

103-
benchmark_questions = [
104-
inquirer.Confirm("benchmark_mode", message="Run GitHub Actions in benchmark mode?", default=True)
105-
]
103+
if skip_confirm:
104+
benchmark_mode = True
105+
else:
106+
benchmark_questions = [
107+
inquirer.Confirm("benchmark_mode", message="Run GitHub Actions in benchmark mode?", default=True)
108+
]
106109

107-
benchmark_answers = inquirer.prompt(benchmark_questions, theme=CodeflashTheme())
108-
benchmark_mode = benchmark_answers["benchmark_mode"] if benchmark_answers else False
110+
benchmark_answers = inquirer.prompt(benchmark_questions, theme=CodeflashTheme())
111+
benchmark_mode = benchmark_answers["benchmark_mode"] if benchmark_answers else False
109112

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

124-
creation_questions = [
125-
inquirer.Confirm(
126-
"confirm_creation",
127-
message="Set up GitHub Actions for continuous optimization? We'll open a pull request with the workflow file.",
128-
default=True,
129-
)
130-
]
127+
if skip_confirm:
128+
confirm_creation = True
129+
else:
130+
creation_questions = [
131+
inquirer.Confirm(
132+
"confirm_creation",
133+
message="Set up GitHub Actions for continuous optimization? We'll open a pull request with the workflow file.",
134+
default=True,
135+
)
136+
]
137+
138+
creation_answers = inquirer.prompt(creation_questions, theme=CodeflashTheme())
139+
confirm_creation = bool(creation_answers and creation_answers["confirm_creation"])
131140

132-
creation_answers = inquirer.prompt(creation_questions, theme=CodeflashTheme())
133-
if not creation_answers or not creation_answers["confirm_creation"]:
141+
if not confirm_creation:
134142
skip_panel = Panel(
135143
Text("⏩️ Skipping GitHub Actions setup.", style="yellow"), title="⏩️ Skipped", border_style="yellow"
136144
)
137145
console.print(skip_panel)
138146
ph("cli-github-workflow-skipped")
139147
return
140-
ph(
141-
"cli-github-optimization-confirm-workflow-creation",
142-
{"confirm_creation": creation_answers["confirm_creation"]},
143-
)
148+
ph("cli-github-optimization-confirm-workflow-creation", {"confirm_creation": confirm_creation})
144149

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

431+
if skip_confirm:
432+
click.echo("Add your CODEFLASH_API_KEY as a GitHub secret before running this workflow.")
433+
ph("cli-github-workflow-created")
434+
return
435+
426436
# Show GitHub secrets setup panel (needed in both cases - PR created via API or local file)
427437
try:
428438
existing_api_key = get_codeflash_api_key()
@@ -555,8 +565,11 @@ def get_github_action_working_directory(toml_path: Path, git_root: Path) -> str:
555565
def detect_project_language_for_workflow(project_root: Path) -> str:
556566
"""Detect the primary language of the project for workflow generation.
557567
558-
Returns: 'python', 'javascript', 'typescript', or 'java'
568+
Returns: 'python', 'javascript', 'typescript', 'java', or 'go'
559569
"""
570+
if (project_root / "go.mod").exists():
571+
return "go"
572+
560573
# Check for Java build tools first (pom.xml or build.gradle)
561574
if (
562575
(project_root / "pom.xml").exists()
@@ -693,9 +706,9 @@ def generate_dynamic_workflow_content(
693706
# Detect project language
694707
project_language = detect_project_language_for_workflow(Path.cwd())
695708

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

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

840+
if project_language == "go":
841+
return _customize_go_workflow_content(optimize_yml_content, git_root, benchmark_mode)
842+
827843
# Python project (default)
828844
return _customize_python_workflow_content(optimize_yml_content, git_root, benchmark_mode)
829845

@@ -948,3 +964,40 @@ def _customize_java_workflow_content(optimize_yml_content: str, git_root: Path)
948964
# Install dependencies command
949965
install_deps = get_java_dependency_installation_commands(build_tool)
950966
return optimize_yml_content.replace("{{ install_dependencies_command }}", install_deps)
967+
968+
969+
def _customize_go_workflow_content(
970+
optimize_yml_content: str, git_root: Path, benchmark_mode: bool = False
971+
) -> str:
972+
"""Customize workflow content for Go projects."""
973+
from codeflash.cli_cmds.init_go import get_go_dependency_installation_commands, get_go_runtime_setup_steps
974+
975+
project_root = Path.cwd()
976+
977+
if project_root == git_root:
978+
working_dir = ""
979+
else:
980+
rel_path = str(project_root.relative_to(git_root))
981+
working_dir = f"""defaults:
982+
run:
983+
working-directory: ./{rel_path}"""
984+
985+
optimize_yml_content = optimize_yml_content.replace("Optimize new Python code", "Optimize new Go code")
986+
optimize_yml_content = optimize_yml_content.replace("{{ working_directory }}", working_dir)
987+
988+
python_setup = get_dependency_manager_installation_string(DependencyManager.PIP)
989+
go_setup = get_go_runtime_setup_steps()
990+
setup_runtime = f"""{python_setup}
991+
{go_setup}"""
992+
optimize_yml_content = optimize_yml_content.replace("{{ setup_runtime_environment }}", setup_runtime)
993+
994+
install_deps = f"""|
995+
python -m pip install --upgrade pip
996+
pip install codeflash
997+
{get_go_dependency_installation_commands()}"""
998+
optimize_yml_content = optimize_yml_content.replace("{{ install_dependencies_command }}", install_deps)
999+
1000+
codeflash_cmd = "codeflash"
1001+
if benchmark_mode:
1002+
codeflash_cmd += " --benchmark"
1003+
return optimize_yml_content.replace("{{ codeflash_command }}", codeflash_cmd)

codeflash/cli_cmds/init_go.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def _get_theme() -> Any:
4040
return CodeflashTheme()
4141

4242

43-
def init_go_project() -> None:
43+
def init_go_project(*, skip_confirm: bool = False, skip_api_key: bool = False) -> None:
4444
from codeflash.cli_cmds.github_workflow import install_github_actions
4545
from codeflash.cli_cmds.init_auth import install_github_app, prompt_api_key
4646

@@ -54,14 +54,14 @@ def init_go_project() -> None:
5454
console.print(lang_panel)
5555
console.print()
5656

57-
did_add_new_key = prompt_api_key()
57+
did_add_new_key = False if skip_api_key else prompt_api_key()
5858

59-
setup_info = collect_go_setup_info()
59+
setup_info = collect_go_setup_info(skip_confirm=skip_confirm)
6060
git_remote = setup_info.git_remote or "origin"
6161

6262
install_github_app(git_remote)
6363

64-
install_github_actions(override_formatter_check=True)
64+
install_github_actions(override_formatter_check=True, skip_confirm=skip_confirm)
6565

6666
usage_table = Table(show_header=False, show_lines=False, border_style="dim")
6767
usage_table.add_column("Command", style="cyan")
@@ -95,7 +95,7 @@ def init_go_project() -> None:
9595
sys.exit(0)
9696

9797

98-
def collect_go_setup_info() -> GoSetupInfo:
98+
def collect_go_setup_info(*, skip_confirm: bool = False) -> GoSetupInfo:
9999

100100
from codeflash.cli_cmds.init_config import ask_for_telemetry
101101

@@ -129,14 +129,18 @@ def collect_go_setup_info() -> GoSetupInfo:
129129
console.print(detection_panel)
130130
console.print()
131131

132+
if skip_confirm:
133+
git_remote = _get_git_remote_for_setup(skip_confirm=True)
134+
return GoSetupInfo(git_remote=git_remote, disable_telemetry=False)
135+
132136
git_remote = _get_git_remote_for_setup()
133137

134138
disable_telemetry = not ask_for_telemetry()
135139

136140
return GoSetupInfo(git_remote=git_remote, disable_telemetry=disable_telemetry)
137141

138142

139-
def _get_git_remote_for_setup() -> str:
143+
def _get_git_remote_for_setup(*, skip_confirm: bool = False) -> str:
140144
try:
141145
repo = Repo(Path.cwd(), search_parent_directories=True)
142146
git_remotes = get_git_remotes(repo)
@@ -145,6 +149,8 @@ def _get_git_remote_for_setup() -> str:
145149

146150
if len(git_remotes) == 1:
147151
return git_remotes[0]
152+
if skip_confirm:
153+
return "origin" if "origin" in git_remotes else git_remotes[0]
148154

149155
git_panel = Panel(
150156
Text(

0 commit comments

Comments
 (0)