From aa6755b9d71a2c8bd60d5cf1942a95fb2f88b925 Mon Sep 17 00:00:00 2001 From: Ace <1160684883@qq.com> Date: Sat, 12 Sep 2026 04:56:20 +0800 Subject: [PATCH] fix(cli): honor editor file opt-outs --- .../repowise/cli/editor_integrations/codex.py | 22 +-- packages/cli/src/repowise/cli/editor_setup.py | 45 ++++++ tests/unit/cli/test_editor_setup.py | 142 ++++++++++++++++-- 3 files changed, 187 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/repowise/cli/editor_integrations/codex.py b/packages/cli/src/repowise/cli/editor_integrations/codex.py index fbdbed557..73c4c6787 100644 --- a/packages/cli/src/repowise/cli/editor_integrations/codex.py +++ b/packages/cli/src/repowise/cli/editor_integrations/codex.py @@ -37,15 +37,16 @@ def write_project_files( setup_override = options.integration_overrides.get(self.integration_id) agents_override = options.project_file_overrides.get(self.project_file_id) agents_override_present = self.project_file_id in options.project_file_overrides + agents_disabled = self.project_file_id in options.disabled_project_files # Codex is opt-in, and stays that way. It is the one integration that # already got this right — nothing is written unless --codex or the # checklist asked for it — which is why #1499 names the other two. if setup_override is None or setup_override is False: - if agents_override_present: - written = maybe_generate_agents_md( + if agents_override_present and not agents_disabled: + agents_path = maybe_generate_agents_md( console_obj, repo_path, agents_md=agents_override ) - return [written] if written is not None else [] + return [agents_path] if agents_path is not None else [] return [] installed = is_codex_cli_installed() @@ -56,13 +57,14 @@ def write_project_files( hooks_path = save_codex_hooks_config(repo_path) console_obj.print(f" [{OK}]✓[/] Codex hooks registered ({hooks_path})") written = [Path(config_path), Path(hooks_path)] - agents_path = maybe_generate_agents_md( - console_obj, - repo_path, - agents_md=True if agents_override is None else agents_override, - ) - if agents_path is not None: - written.append(agents_path) + if not agents_disabled: + agents_path = maybe_generate_agents_md( + console_obj, + repo_path, + agents_md=True if agents_override is None else agents_override, + ) + if agents_path is not None: + written.append(agents_path) if not installed: console_obj.print( diff --git a/packages/cli/src/repowise/cli/editor_setup.py b/packages/cli/src/repowise/cli/editor_setup.py index 95dd5f2a3..26bd8c31e 100644 --- a/packages/cli/src/repowise/cli/editor_setup.py +++ b/packages/cli/src/repowise/cli/editor_setup.py @@ -197,6 +197,48 @@ def resolve_editor_setup_options( ) +def _resolve_configured_project_file_optouts( + repo_path: Path, + options: EditorSetupOptions, +) -> EditorSetupOptions: + """Merge explicit ``editor_files: : false`` values into *options*. + + The project-file preference belongs to the setup layer, not to an editor + integration. Resolving it here gives every write path the same view and + lets a newly registered target reuse the descriptor's ``project_file_id`` + without adding another config reader beside its writer. + + An explicit per-run project-file override remains stronger than the + persisted preference. This preserves ``--agents`` / ``--no-agents`` as + command-line overrides while still treating a missing key as "let the + integration apply its own default" (notably, ``agents_md`` defaults off on + the update path). + """ + from repowise.cli.agent_targets.registry import all_targets + from repowise.cli.helpers import load_config + + editor_files = load_config(repo_path).get("editor_files") + if not isinstance(editor_files, Mapping): + return options + + configured_disabled = { + target.project_file_id + for target in all_targets() + if editor_files.get(target.project_file_id) is False + } + disabled_project_files = ( + options.disabled_project_files | frozenset(configured_disabled) + ) - options.project_file_overrides.keys() + if disabled_project_files == options.disabled_project_files: + return options + + return EditorSetupOptions( + disabled_project_files=frozenset(disabled_project_files), + project_file_overrides=dict(options.project_file_overrides), + integration_overrides=dict(options.integration_overrides), + ) + + def select_agents_interactively( console_obj: Any, repo_path: Path, @@ -322,6 +364,8 @@ def write_editor_project_files( _persist_project_file_optouts(repo_path, resolved_options) return [] + resolved_options = _resolve_configured_project_file_optouts(repo_path, resolved_options) + written: list[Path] = [] for integration in _resolve_integrations(integrations): # ``or []`` rather than a required return: an integration that has @@ -380,5 +424,6 @@ def refresh_editor_project_files( """Refresh editor-managed project files without rewriting common MCP config.""" resolved_options = options or EditorSetupOptions() + resolved_options = _resolve_configured_project_file_optouts(repo_path, resolved_options) for integration in _resolve_integrations(integrations): integration.refresh_project_files(console_obj, repo_path, resolved_options) diff --git a/tests/unit/cli/test_editor_setup.py b/tests/unit/cli/test_editor_setup.py index b258128ff..77a5d0261 100644 --- a/tests/unit/cli/test_editor_setup.py +++ b/tests/unit/cli/test_editor_setup.py @@ -22,6 +22,7 @@ get_default_integration_overrides, get_default_project_file_overrides, ) +from repowise.cli.editor_integrations.vscode import VSCodeSetup from repowise.cli.editor_setup import ( EditorSetupOptions, refresh_editor_project_files, @@ -302,9 +303,7 @@ def test_no_editor_setup_turns_off_every_replacing_surface(monkeypatch, tmp_path (tmp_path / ".repowise").mkdir() - offer_distill_rewrite_hook( - _silent_console(), [tmp_path], False, yes=True, no_editor_setup=True - ) + offer_distill_rewrite_hook(_silent_console(), [tmp_path], False, yes=True, no_editor_setup=True) assert _all_same(_hook_verdicts(tmp_path), False) @@ -425,9 +424,7 @@ def _fake(console_obj, choices): return answer(choices) if callable(answer) else answer monkeypatch.setattr(agent_selection, "interactive_agent_select", _fake) - options = select_agents_interactively( - _silent_console(), tmp_path, EditorSetupOptions(**kwargs) - ) + options = select_agents_interactively(_silent_console(), tmp_path, EditorSetupOptions(**kwargs)) return options, seen[0] @@ -442,9 +439,7 @@ def test_checklist_unticking_an_agent_disables_its_project_file(monkeypatch, tmp def test_checklist_ticking_everything_disables_nothing(monkeypatch, tmp_path) -> None: - options, _ = _select( - monkeypatch, tmp_path, lambda choices: {choice.id for choice in choices} - ) + options, _ = _select(monkeypatch, tmp_path, lambda choices: {choice.id for choice in choices}) from repowise.cli.editor_integrations.defaults import get_default_editor_integrations @@ -482,9 +477,7 @@ def test_checklist_pre_ticks_an_agent_an_explicit_flag_asked_for(monkeypatch, tm Otherwise accepting the checklist would silently undo the flag the user passed on the same command line. """ - _, choices = _select( - monkeypatch, tmp_path, set(), integration_overrides={"codex": True} - ) + _, choices = _select(monkeypatch, tmp_path, set(), integration_overrides={"codex": True}) codex = next(choice for choice in choices if choice.id == "codex") assert codex.enabled is True @@ -690,6 +683,131 @@ def refresh_project_files( assert calls == [("refresh", tmp_path, frozenset({"skip"}))] +def test_write_editor_project_files_honors_vscode_config_optout( + tmp_path: Path, + monkeypatch: Any, +) -> None: + """The init write path resolves an explicit VS Code opt-out centrally.""" + monkeypatch.delenv("REPOWISE_SKIP_EDITOR_SETUP", raising=False) + repowise_dir = tmp_path / ".repowise" + repowise_dir.mkdir() + (repowise_dir / "config.yaml").write_text( + "editor_files:\n vscode_mcp: false\n", + encoding="utf-8", + ) + monkeypatch.setattr( + mcp_config, + "save_mcp_config", + lambda repo_path: repo_path / ".repowise" / "mcp.json", + ) + + written = write_editor_project_files( + _silent_console(), + tmp_path, + integrations=(VSCodeSetup(),), + ) + + assert written == [] + assert not (tmp_path / ".vscode").exists() + + +def test_refresh_editor_project_files_honors_vscode_config_optout(tmp_path: Path) -> None: + """The update refresh path applies the same explicit opt-out.""" + repowise_dir = tmp_path / ".repowise" + repowise_dir.mkdir() + (repowise_dir / "config.yaml").write_text( + "editor_files:\n vscode_mcp: false\n", + encoding="utf-8", + ) + + refresh_editor_project_files( + _silent_console(), + tmp_path, + integrations=(VSCodeSetup(),), + ) + + assert not (tmp_path / ".vscode").exists() + + +def test_explicit_project_file_override_wins_over_configured_optout( + tmp_path: Path, + monkeypatch: Any, +) -> None: + """A per-run override wins even when a disabled set also contains the id.""" + monkeypatch.delenv("REPOWISE_SKIP_EDITOR_SETUP", raising=False) + repowise_dir = tmp_path / ".repowise" + repowise_dir.mkdir() + (repowise_dir / "config.yaml").write_text( + "editor_files:\n agents_md: false\n", + encoding="utf-8", + ) + calls: list[bool | None] = [] + monkeypatch.setattr( + mcp_config, + "save_mcp_config", + lambda repo_path: repo_path / ".repowise" / "mcp.json", + ) + monkeypatch.setattr( + codex_integration, + "maybe_generate_agents_md", + lambda _console, _repo_path, *, agents_md=None: calls.append(agents_md), + ) + + write_editor_project_files( + _silent_console(), + tmp_path, + options=EditorSetupOptions( + disabled_project_files=frozenset({"agents_md"}), + project_file_overrides={"agents_md": True}, + ), + integrations=(CodexSetup(),), + ) + + assert calls == [True] + + +def test_write_editor_project_files_honors_agents_config_optout_when_codex_enabled( + tmp_path: Path, + monkeypatch: Any, +) -> None: + """An AGENTS.md opt-out must not disable Codex MCP registration.""" + monkeypatch.delenv("REPOWISE_SKIP_EDITOR_SETUP", raising=False) + repowise_dir = tmp_path / ".repowise" + repowise_dir.mkdir() + (repowise_dir / "config.yaml").write_text( + "editor_files:\n agents_md: false\n", + encoding="utf-8", + ) + monkeypatch.setattr( + mcp_config, + "save_mcp_config", + lambda repo_path: repo_path / ".repowise" / "mcp.json", + ) + calls: list[str] = [] + monkeypatch.setattr( + mcp_config, + "save_codex_mcp_config", + lambda repo_path: calls.append("mcp") or repo_path / ".codex" / "config.toml", + ) + monkeypatch.setattr( + mcp_config, + "save_codex_hooks_config", + lambda repo_path: calls.append("hooks") or repo_path / ".codex" / "hooks.toml", + ) + monkeypatch.setattr(mcp_config, "is_codex_cli_installed", lambda: False) + monkeypatch.setattr(codex_integration, "maybe_generate_agents_md", lambda *args, **kwargs: None) + + written = write_editor_project_files( + _silent_console(), + tmp_path, + options=EditorSetupOptions(integration_overrides={"codex": True}), + integrations=(CodexSetup(),), + ) + + assert calls == ["mcp", "hooks"] + assert written == [tmp_path / ".codex" / "config.toml", tmp_path / ".codex" / "hooks.toml"] + + def _write_settings(path: Path, entry: dict) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps({"mcpServers": {"repowise": entry}}), encoding="utf-8")