Skip to content

Commit ac47f8b

Browse files
fix: handle non-tty init prompts cleanly
1 parent 62fd795 commit ac47f8b

8 files changed

Lines changed: 159 additions & 19 deletions

File tree

codeflash/cli_cmds/github_workflow.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class DependencyManager(Enum):
3737
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)
40+
interactive_stdin = sys.stdin.isatty()
4041

4142
ph("cli-github-actions-install-started")
4243
try:
@@ -102,6 +103,8 @@ def install_github_actions(override_formatter_check: bool = False, *, skip_confi
102103

103104
if skip_confirm:
104105
benchmark_mode = True
106+
elif not interactive_stdin:
107+
benchmark_mode = False
105108
else:
106109
benchmark_questions = [
107110
inquirer.Confirm("benchmark_mode", message="Run GitHub Actions in benchmark mode?", default=True)
@@ -126,6 +129,8 @@ def install_github_actions(override_formatter_check: bool = False, *, skip_confi
126129

127130
if skip_confirm:
128131
confirm_creation = True
132+
elif not interactive_stdin:
133+
confirm_creation = False
129134
else:
130135
creation_questions = [
131136
inquirer.Confirm(

codeflash/cli_cmds/init_config.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,18 @@ def get_toml_key(self) -> str:
8080
}
8181

8282

83+
def confirm_with_default_on_eof(
84+
prompt: str, *, default: bool, show_default: bool = True, **kwargs: Any
85+
) -> bool:
86+
"""Return the prompt default instead of crashing when stdin is unavailable."""
87+
from rich.prompt import Confirm
88+
89+
try:
90+
return Confirm.ask(prompt, default=default, show_default=show_default, **kwargs)
91+
except EOFError:
92+
return default
93+
94+
8395
@lru_cache(maxsize=1)
8496
def get_valid_subdirs(current_dir: Optional[Path] = None) -> list[str]:
8597

@@ -148,8 +160,6 @@ def should_modify_pyproject_toml(*, skip_confirm: bool = False) -> tuple[bool, d
148160
149161
If it does, ask the user if they want to re-configure it.
150162
"""
151-
from rich.prompt import Confirm
152-
153163
pyproject_toml_path = Path.cwd() / "pyproject.toml"
154164

155165
found, _ = config_found(pyproject_toml_path)
@@ -164,7 +174,7 @@ def should_modify_pyproject_toml(*, skip_confirm: bool = False) -> tuple[bool, d
164174
if skip_confirm:
165175
return False, config
166176

167-
return Confirm.ask(
177+
return confirm_with_default_on_eof(
168178
"✅ A valid Codeflash config already exists in this project. Do you want to re-configure it?",
169179
default=False,
170180
show_default=True,
@@ -285,9 +295,7 @@ def create_empty_pyproject_toml(pyproject_toml_path: Path) -> None:
285295

286296
def ask_for_telemetry() -> bool:
287297
"""Prompt the user to enable or disable telemetry."""
288-
from rich.prompt import Confirm
289-
290-
return Confirm.ask(
298+
return confirm_with_default_on_eof(
291299
"⚡️ Help us improve Codeflash by sharing anonymous usage data (e.g. errors encountered)?",
292300
default=True,
293301
show_default=True,

codeflash/cli_cmds/init_java.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from rich.text import Text
2121

2222
from codeflash.cli_cmds.console import apologize_and_exit, console
23+
from codeflash.cli_cmds.init_config import confirm_with_default_on_eof
2324
from codeflash.code_utils.code_utils import validate_relative_directory_path
2425
from codeflash.code_utils.compat import LF
2526
from codeflash.code_utils.git_utils import get_git_remotes
@@ -215,8 +216,6 @@ def init_java_project(*, skip_confirm: bool = False, skip_api_key: bool = False)
215216

216217
def should_modify_java_config(*, skip_confirm: bool = False) -> tuple[bool, dict[str, Any] | None]:
217218
"""Check if the project already has Codeflash config."""
218-
from rich.prompt import Confirm
219-
220219
project_root = Path.cwd()
221220

222221
# Check for existing codeflash config in pom.xml properties or gradle.properties
@@ -228,7 +227,7 @@ def should_modify_java_config(*, skip_confirm: bool = False) -> tuple[bool, dict
228227
if existing:
229228
if skip_confirm:
230229
return False, None
231-
return Confirm.ask(
230+
return confirm_with_default_on_eof(
232231
"A Codeflash config already exists. Do you want to re-configure it?", default=False, show_default=True
233232
), None
234233
except ValueError:
@@ -239,8 +238,6 @@ def should_modify_java_config(*, skip_confirm: bool = False) -> tuple[bool, dict
239238

240239
def collect_java_setup_info(*, skip_confirm: bool = False) -> JavaSetupInfo:
241240
"""Collect setup information for Java projects."""
242-
from rich.prompt import Confirm
243-
244241
from codeflash.cli_cmds.init_config import ask_for_telemetry
245242

246243
curdir = Path.cwd()
@@ -282,7 +279,7 @@ def collect_java_setup_info(*, skip_confirm: bool = False) -> JavaSetupInfo:
282279
test_root_override = None
283280
formatter_override = None
284281

285-
if Confirm.ask("Would you like to change any of these settings?", default=False):
282+
if confirm_with_default_on_eof("Would you like to change any of these settings?", default=False):
286283
# Source root override
287284
module_root_override = _prompt_directory_override("source", detected_source_root, curdir)
288285

codeflash/cli_cmds/init_javascript.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from git import InvalidGitRepositoryError, Repo
2020
from rich.console import Group
2121
from rich.panel import Panel
22-
from rich.prompt import Confirm
2322
from rich.table import Table
2423
from rich.text import Text
2524

@@ -297,6 +296,8 @@ def init_js_project(language: ProjectLanguage, *, skip_confirm: bool = False, sk
297296

298297
def should_modify_package_json_config(*, skip_confirm: bool = False) -> tuple[bool, dict[str, Any] | None]:
299298
"""Check if package.json has valid codeflash config for JS/TS projects."""
299+
from codeflash.cli_cmds.init_config import confirm_with_default_on_eof
300+
300301
package_json_path = Path("package.json")
301302

302303
if not package_json_path.exists():
@@ -326,7 +327,7 @@ def should_modify_package_json_config(*, skip_confirm: bool = False) -> tuple[bo
326327
return False, config
327328

328329
# Config is valid - ask if user wants to reconfigure
329-
return Confirm.ask(
330+
return confirm_with_default_on_eof(
330331
"✅ A valid Codeflash config already exists in package.json. Do you want to re-configure it?",
331332
default=False,
332333
show_default=True,
@@ -341,7 +342,7 @@ def collect_js_setup_info(language: ProjectLanguage, *, skip_confirm: bool = Fal
341342
Uses auto-detection for most settings and only asks for overrides if needed.
342343
When skip_confirm is True, uses all auto-detected defaults without prompting.
343344
"""
344-
from codeflash.cli_cmds.init_config import ask_for_telemetry, get_valid_subdirs
345+
from codeflash.cli_cmds.init_config import ask_for_telemetry, confirm_with_default_on_eof, get_valid_subdirs
345346
from codeflash.code_utils.config_js import (
346347
detect_formatter,
347348
detect_module_root,
@@ -378,8 +379,6 @@ def collect_js_setup_info(language: ProjectLanguage, *, skip_confirm: bool = Fal
378379
pass
379380
return JSSetupInfo(git_remote=git_remote)
380381

381-
from rich.prompt import Confirm
382-
383382
# Build detection summary
384383
formatter_display = detected_formatter[0] if detected_formatter else "none detected"
385384
detection_table = Table(show_header=False, box=None, padding=(0, 2))
@@ -401,7 +400,7 @@ def collect_js_setup_info(language: ProjectLanguage, *, skip_confirm: bool = Fal
401400
module_root_override = None
402401
formatter_override = None
403402

404-
if Confirm.ask("Would you like to change any of these settings?", default=False):
403+
if confirm_with_default_on_eof("Would you like to change any of these settings?", default=False):
405404
# Module root override
406405
valid_subdirs = get_valid_subdirs()
407406
curdir_option = f"current directory ({curdir})"

tests/test_github_workflow.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,27 @@ def test_install_github_actions_skip_confirm_supports_go_projects(tmp_path: Path
5353
assert "Optimize new Go code" in workflow_text
5454
assert "actions/setup-go@v5" in workflow_text
5555
assert "go mod download" in workflow_text
56+
57+
58+
def test_install_github_actions_non_tty_skips_optional_setup(tmp_path: Path, monkeypatch) -> None:
59+
monkeypatch.chdir(tmp_path)
60+
Repo.init(tmp_path)
61+
(tmp_path / "tests").mkdir()
62+
(tmp_path / "pyproject.toml").write_text(
63+
"""
64+
[tool.codeflash]
65+
module-root = "."
66+
tests-root = "tests"
67+
formatter-cmds = ["disabled"]
68+
""".strip(),
69+
encoding="utf-8",
70+
)
71+
72+
stdin = type("Stdin", (), {"isatty": lambda self: False})()
73+
monkeypatch.setattr("codeflash.cli_cmds.github_workflow.sys.stdin", stdin)
74+
75+
with patch("codeflash.cli_cmds.github_workflow.inquirer.prompt") as mock_prompt:
76+
install_github_actions()
77+
78+
mock_prompt.assert_not_called()
79+
assert not (tmp_path / ".github" / "workflows" / "codeflash.yaml").exists()

tests/test_init_java_go.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,21 @@ def test_collect_go_setup_info_skip_confirm_uses_defaults(tmp_path: Path, monkey
2727
assert setup_info.disable_telemetry is False
2828

2929

30+
def test_collect_go_setup_info_uses_default_telemetry_on_eof(tmp_path: Path, monkeypatch) -> None:
31+
monkeypatch.chdir(tmp_path)
32+
(tmp_path / "go.mod").write_text("module example.com/demo\n\ngo 1.21\n", encoding="utf-8")
33+
34+
get_git_remote = Mock(return_value="origin")
35+
monkeypatch.setattr("codeflash.cli_cmds.init_go._get_git_remote_for_setup", get_git_remote)
36+
37+
with patch("rich.prompt.Confirm.ask", side_effect=EOFError):
38+
setup_info = collect_go_setup_info()
39+
40+
get_git_remote.assert_called_once_with()
41+
assert setup_info.git_remote == "origin"
42+
assert setup_info.disable_telemetry is False
43+
44+
3045
def test_collect_java_setup_info_skip_confirm_uses_defaults(tmp_path: Path, monkeypatch) -> None:
3146
monkeypatch.chdir(tmp_path)
3247
(tmp_path / "build.gradle").write_text("plugins { id 'java' }\n", encoding="utf-8")
@@ -52,6 +67,30 @@ def test_collect_java_setup_info_skip_confirm_uses_defaults(tmp_path: Path, monk
5267
assert setup_info.disable_telemetry is False
5368

5469

70+
def test_collect_java_setup_info_uses_defaults_on_eof(tmp_path: Path, monkeypatch) -> None:
71+
monkeypatch.chdir(tmp_path)
72+
(tmp_path / "build.gradle").write_text("plugins { id 'java' }\n", encoding="utf-8")
73+
(tmp_path / "src" / "main" / "java").mkdir(parents=True)
74+
(tmp_path / "src" / "test" / "java").mkdir(parents=True)
75+
76+
get_git_remote = Mock(return_value="origin")
77+
monkeypatch.setattr("codeflash.cli_cmds.init_java._get_git_remote_for_setup", get_git_remote)
78+
monkeypatch.setattr("codeflash.cli_cmds.init_config.ask_for_telemetry", Mock(return_value=True))
79+
80+
with patch("codeflash.cli_cmds.init_java.inquirer") as mock_inquirer, patch(
81+
"rich.prompt.Confirm.ask", side_effect=EOFError
82+
):
83+
setup_info = collect_java_setup_info()
84+
85+
mock_inquirer.prompt.assert_not_called()
86+
get_git_remote.assert_called_once_with()
87+
assert setup_info.module_root_override is None
88+
assert setup_info.test_root_override is None
89+
assert setup_info.formatter_override is None
90+
assert setup_info.git_remote == "origin"
91+
assert setup_info.disable_telemetry is False
92+
93+
5594
def test_should_modify_java_config_skip_confirm_skips_reconfigure_prompt(tmp_path: Path, monkeypatch) -> None:
5695
monkeypatch.chdir(tmp_path)
5796
(tmp_path / "build.gradle").write_text("plugins { id 'java' }\n", encoding="utf-8")
@@ -62,3 +101,15 @@ def test_should_modify_java_config_skip_confirm_skips_reconfigure_prompt(tmp_pat
62101

63102
assert should_modify is False
64103
assert config is None
104+
105+
106+
def test_should_modify_java_config_uses_default_on_eof(tmp_path: Path, monkeypatch) -> None:
107+
monkeypatch.chdir(tmp_path)
108+
(tmp_path / "build.gradle").write_text("plugins { id 'java' }\n", encoding="utf-8")
109+
(tmp_path / "gradle.properties").write_text("codeflash.moduleRoot=src/main/java\n", encoding="utf-8")
110+
111+
with patch("rich.prompt.Confirm.ask", side_effect=EOFError):
112+
should_modify, config = should_modify_java_config()
113+
114+
assert should_modify is False
115+
assert config is None

tests/test_init_javascript.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
import json
44
import tempfile
55
from pathlib import Path
6-
from unittest.mock import patch
6+
from unittest.mock import Mock, patch
77

88
import pytest
99

1010
from codeflash.cli_cmds.init_javascript import (
1111
JsPackageManager,
1212
ProjectLanguage,
13+
collect_js_setup_info,
1314
detect_project_language,
1415
determine_js_package_manager,
1516
get_package_install_command,
@@ -334,6 +335,20 @@ def test_should_modify_skip_confirm_with_invalid_config(
334335
assert should_modify is True
335336
assert config is None
336337

338+
def test_should_modify_valid_config_uses_default_on_eof(
339+
self, tmp_project: Path, monkeypatch: pytest.MonkeyPatch
340+
) -> None:
341+
"""EOF on the reconfigure prompt should keep the existing config."""
342+
monkeypatch.chdir(tmp_project)
343+
codeflash_config = {"moduleRoot": "."}
344+
(tmp_project / "package.json").write_text(json.dumps({"name": "test", "codeflash": codeflash_config}))
345+
346+
with patch("rich.prompt.Confirm.ask", side_effect=EOFError):
347+
should_modify, config = should_modify_package_json_config()
348+
349+
assert should_modify is False
350+
assert config == codeflash_config
351+
337352

338353
class TestCollectJsSetupInfoSkipConfirm:
339354
"""Tests for collect_js_setup_info with skip_confirm."""
@@ -354,6 +369,30 @@ def test_collect_js_setup_info_skip_confirm(self, tmp_project: Path, monkeypatch
354369
assert setup_info.formatter_override is None
355370
assert setup_info.git_remote == "origin"
356371

372+
def test_collect_js_setup_info_uses_defaults_on_eof(
373+
self, tmp_project: Path, monkeypatch: pytest.MonkeyPatch
374+
) -> None:
375+
"""EOF on the settings confirm should keep auto-detected defaults."""
376+
monkeypatch.chdir(tmp_project)
377+
(tmp_project / "package.json").write_text(json.dumps({"name": "test"}))
378+
379+
get_git_remote = Mock(return_value="origin")
380+
monkeypatch.setattr("codeflash.cli_cmds.init_javascript._get_git_remote_for_setup", get_git_remote)
381+
monkeypatch.setattr("codeflash.cli_cmds.init_config.ask_for_telemetry", Mock(return_value=True))
382+
383+
with (
384+
patch("rich.prompt.Confirm.ask", side_effect=EOFError),
385+
patch("codeflash.cli_cmds.init_javascript.inquirer") as mock_inquirer,
386+
):
387+
setup_info = collect_js_setup_info(ProjectLanguage.JAVASCRIPT)
388+
389+
mock_inquirer.prompt.assert_not_called()
390+
get_git_remote.assert_called_once_with()
391+
assert setup_info.module_root_override is None
392+
assert setup_info.formatter_override is None
393+
assert setup_info.git_remote == "origin"
394+
assert setup_info.disable_telemetry is False
395+
357396

358397
class TestDetectProjectLanguage:
359398
"""Tests for detect_project_language function."""

tests/test_init_yes.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,23 @@ def test_should_modify_pyproject_toml_skip_confirm_skips_reconfigure_prompt(
6666
assert config["git_remote"] == "upstream"
6767

6868

69+
def test_should_modify_pyproject_toml_uses_default_on_eof(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
70+
monkeypatch.chdir(tmp_path)
71+
(tmp_path / "src").mkdir()
72+
(tmp_path / "tests").mkdir()
73+
(tmp_path / "pyproject.toml").write_text(
74+
'[tool.codeflash]\nmodule-root = "src"\ntests-root = "tests"\ngit-remote = "upstream"\n',
75+
encoding="utf-8",
76+
)
77+
78+
with patch("rich.prompt.Confirm.ask", side_effect=EOFError):
79+
should_modify, config = should_modify_pyproject_toml()
80+
81+
assert should_modify is False
82+
assert config is not None
83+
assert config["git_remote"] == "upstream"
84+
85+
6986
def test_init_codeflash_skip_confirm_reuses_existing_python_config(
7087
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
7188
) -> None:

0 commit comments

Comments
 (0)