Skip to content
Merged
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
22 changes: 12 additions & 10 deletions packages/cli/src/repowise/cli/editor_integrations/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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(
Expand Down
45 changes: 45 additions & 0 deletions packages/cli/src/repowise/cli/editor_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: <id>: 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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
142 changes: 130 additions & 12 deletions tests/unit/cli/test_editor_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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]


Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
Loading