code-agnostic 0.5.1__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/PKG-INFO +1 -1
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/__init__.py +1 -1
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/apps_service.py +45 -10
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/claude/service.py +19 -2
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/codex/service.py +17 -5
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/service.py +8 -1
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/service.py +4 -1
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/executor.py +8 -6
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/models.py +1 -1
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/planner.py +18 -4
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic.egg-info/PKG-INFO +1 -1
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic.egg-info/SOURCES.txt +1 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/pyproject.toml +1 -1
- code_agnostic-0.7.0/tests/test_cli_apply_codex_agent_cleanup.py +71 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_disable_cleanup.py +48 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_workspace_config_sync.py +77 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/LICENSE +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/README.md +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/__main__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/agents/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/agents/claude.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/agents/codex.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/agents/compilers.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/agents/copilot.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/agents/models.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/agents/opencode.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/agents/parser.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/app_id.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/claude/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/claude/config_repository.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/claude/mapper.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/codex/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/codex/config_repository.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/codex/mapper.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/codex/schema.json +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/codex/schema_repository.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/common/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/common/compiled_planning.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/common/framework.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/common/loader.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/common/models.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/common/schema.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/common/symlink_planning.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/common/utils.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/copilot/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/copilot/config_repository.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/copilot/mapper.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/copilot/service.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/config_repository.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/mapper.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/schema.json +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/schema_repository.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/service.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/config_repository.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/mapper.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/schema.json +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/schema_repository.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/aliases.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/agents.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/apply.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/apps.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/import_.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/mcp.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/plan.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/projects.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/restore.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/rules.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/skills.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/status.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/validate.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/commands/workspaces.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/helpers.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/cli/options.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/constants.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/core/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/core/project_repository.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/core/repository.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/core/workspace_repository.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/errors.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/generated_artifacts.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/git_exclude_service.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/imports/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/imports/adapters.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/imports/filesystem.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/imports/models.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/imports/service.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/lossiness.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/mcp_service.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/project_artifacts.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/rules/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/rules/compilers.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/rules/models.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/rules/parser.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/rules/repository.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/skills/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/skills/compilers.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/skills/install_sources.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/skills/models.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/skills/parser.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/spec/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/spec/loaders.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/status.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/tui/__init__.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/tui/enums.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/tui/import_selector.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/tui/renderers.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/tui/sections.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/tui/tables.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/utils.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/validation.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/workspace_artifacts.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/workspaces.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic.egg-info/dependency_links.txt +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic.egg-info/entry_points.txt +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic.egg-info/requires.txt +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic.egg-info/top_level.txt +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/setup.cfg +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_agents.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_aliases.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_apply_apps.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_apply_codex.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_apply_cursor.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_apply_mcp_cleanup.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_apply_target.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_apps.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_explain_lossiness.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_flags.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_git_exclude.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_import.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_import_interactive.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_mcp.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_module_organization.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_plan.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_projects.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_restore.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_rules.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_skills.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_status.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_validate.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_workspace_resolution.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_cli_workspaces.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_common_mcp_to_dto.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_common_repository.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_compiled_planning.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_dto_to_common_mcp.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_git_exclude_service.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_mcp_service.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_planner_executor.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_planner_rules.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_project_config_sync.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_skill_install_sources.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_symlink_planning.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_sync_plan_model.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_transactional_executor.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_utils.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_version.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_workspace_repo_status.py +0 -0
- {code_agnostic-0.5.1 → code_agnostic-0.7.0}/tests/test_workspaces.py +0 -0
|
@@ -6,6 +6,7 @@ from code_agnostic.apps.common.framework import (
|
|
|
6
6
|
create_registered_app_service,
|
|
7
7
|
list_registered_app_services,
|
|
8
8
|
)
|
|
9
|
+
from code_agnostic.apps.claude.service import ClaudeConfigService
|
|
9
10
|
from code_agnostic.apps.common.interfaces.service import IAppConfigService
|
|
10
11
|
from code_agnostic.apps.common.symlink_planning import (
|
|
11
12
|
load_state_links,
|
|
@@ -113,6 +114,8 @@ class AppsService:
|
|
|
113
114
|
self._cleanup_paths(global_paths, scope, app_id.value, skipped)
|
|
114
115
|
)
|
|
115
116
|
actions.extend(self._cleanup_global_mcp(app_id, global_mcp))
|
|
117
|
+
if app_id == AppId.CLAUDE:
|
|
118
|
+
actions.extend(self._cleanup_claude_projects(global_mcp))
|
|
116
119
|
|
|
117
120
|
# --- Workspace scopes (ws:<app>:*) ---
|
|
118
121
|
ws_prefix = f"ws:{app_id.value}:"
|
|
@@ -201,14 +204,11 @@ class AppsService:
|
|
|
201
204
|
def _cleanup_global_mcp(
|
|
202
205
|
self, app_id: AppId, managed_mcp: dict[str, Any]
|
|
203
206
|
) -> list[Action]:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
{item for item in raw if isinstance(item, str)}
|
|
208
|
-
if isinstance(raw, list)
|
|
209
|
-
else set()
|
|
207
|
+
managed = self._names_for_scope(managed_mcp, app_scope(app_id, "mcp"))
|
|
208
|
+
managed_agents = self._names_for_scope(
|
|
209
|
+
managed_mcp, app_scope(app_id, "agents_registry")
|
|
210
210
|
)
|
|
211
|
-
if not managed:
|
|
211
|
+
if not managed and not managed_agents:
|
|
212
212
|
return []
|
|
213
213
|
try:
|
|
214
214
|
service = create_registered_app_service(app_id)
|
|
@@ -216,9 +216,44 @@ class AppsService:
|
|
|
216
216
|
return []
|
|
217
217
|
if not service.repository.config_path.exists():
|
|
218
218
|
return []
|
|
219
|
-
# Empty desired + our previously-managed names → prune only
|
|
220
|
-
# keep the user's, and clear
|
|
221
|
-
|
|
219
|
+
# Empty desired + our previously-managed names → prune only what we wrote
|
|
220
|
+
# (MCP servers and agent-registry entries), keep the user's, and clear the
|
|
221
|
+
# ownership state (managed_entries becomes empty).
|
|
222
|
+
return [
|
|
223
|
+
service.build_action(
|
|
224
|
+
{},
|
|
225
|
+
previously_managed=managed,
|
|
226
|
+
previously_managed_agents=managed_agents,
|
|
227
|
+
)
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
def _cleanup_claude_projects(self, managed_mcp: dict[str, Any]) -> list[Action]:
|
|
231
|
+
managed = self._names_for_scope(
|
|
232
|
+
managed_mcp, app_scope(AppId.CLAUDE, "projects")
|
|
233
|
+
)
|
|
234
|
+
if not managed:
|
|
235
|
+
return []
|
|
236
|
+
try:
|
|
237
|
+
service = create_registered_app_service(AppId.CLAUDE)
|
|
238
|
+
except (KeyError, ValueError):
|
|
239
|
+
return []
|
|
240
|
+
if not isinstance(service, ClaudeConfigService):
|
|
241
|
+
return []
|
|
242
|
+
if not service.repository.config_path.exists():
|
|
243
|
+
return []
|
|
244
|
+
# Empty desired + our previously-managed project paths → prune only the
|
|
245
|
+
# `mcpServers` sub-keys we wrote, keep the rest of each project entry, and
|
|
246
|
+
# clear the ownership state (managed_entries becomes empty).
|
|
247
|
+
return [
|
|
248
|
+
service.build_project_mcp_action({}, previously_managed_projects=managed)
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def _names_for_scope(group: dict[str, Any], scope: str) -> set[str]:
|
|
253
|
+
raw = group.get(scope, [])
|
|
254
|
+
if not isinstance(raw, list):
|
|
255
|
+
return set()
|
|
256
|
+
return {item for item in raw if isinstance(item, str)}
|
|
222
257
|
|
|
223
258
|
def list_status_rows(self) -> list[AppStatusRow]:
|
|
224
259
|
apps = self.load_apps()
|
|
@@ -5,7 +5,7 @@ from typing import Any
|
|
|
5
5
|
from code_agnostic.agents.claude import claude_agent_target_path
|
|
6
6
|
from code_agnostic.agents.compilers import ClaudeAgentCompiler
|
|
7
7
|
from code_agnostic.agents.parser import parse_agent
|
|
8
|
-
from code_agnostic.apps.app_id import AppId, app_label
|
|
8
|
+
from code_agnostic.apps.app_id import AppId, app_label, app_scope
|
|
9
9
|
from code_agnostic.apps.claude.config_repository import ClaudeConfigRepository
|
|
10
10
|
from code_agnostic.apps.claude.mapper import ClaudeMCPMapper
|
|
11
11
|
from code_agnostic.apps.common.framework import RegisteredAppConfigService
|
|
@@ -72,6 +72,7 @@ class ClaudeConfigService(RegisteredAppConfigService):
|
|
|
72
72
|
self,
|
|
73
73
|
project_servers: dict[Path, dict[str, MCPServerDTO]],
|
|
74
74
|
base_payload: dict[str, Any] | None = None,
|
|
75
|
+
previously_managed_projects: set[str] | None = None,
|
|
75
76
|
) -> Action:
|
|
76
77
|
existing = self._repository.load_config()
|
|
77
78
|
if existing or self.repository.config_path.exists():
|
|
@@ -86,8 +87,10 @@ class ClaudeConfigService(RegisteredAppConfigService):
|
|
|
86
87
|
else:
|
|
87
88
|
projects = deepcopy(projects)
|
|
88
89
|
|
|
90
|
+
desired_keys: set[str] = set()
|
|
89
91
|
for project_path, servers in project_servers.items():
|
|
90
92
|
key = str(project_path.resolve())
|
|
93
|
+
desired_keys.add(key)
|
|
91
94
|
project = projects.get(key)
|
|
92
95
|
if not isinstance(project, dict):
|
|
93
96
|
project = {}
|
|
@@ -96,10 +99,18 @@ class ClaudeConfigService(RegisteredAppConfigService):
|
|
|
96
99
|
project["mcpServers"] = self.mapper.from_common(servers)
|
|
97
100
|
projects[key] = project
|
|
98
101
|
|
|
102
|
+
# Prune the `mcpServers` sub-key from project entries we previously wrote
|
|
103
|
+
# but no longer sync (e.g. a repo removed from a workspace). Leave the rest
|
|
104
|
+
# of the project entry (history etc.) and non-managed projects untouched.
|
|
105
|
+
for stale_key in (previously_managed_projects or set()) - desired_keys:
|
|
106
|
+
stale_project = projects.get(stale_key)
|
|
107
|
+
if isinstance(stale_project, dict):
|
|
108
|
+
stale_project.pop("mcpServers", None)
|
|
109
|
+
|
|
99
110
|
merged["projects"] = projects
|
|
100
111
|
self.validate_config(merged)
|
|
101
112
|
|
|
102
|
-
|
|
113
|
+
action = Action(
|
|
103
114
|
kind=self.action_kind,
|
|
104
115
|
path=self.repository.config_path,
|
|
105
116
|
status=self.derive_status(existing, merged),
|
|
@@ -107,6 +118,12 @@ class ClaudeConfigService(RegisteredAppConfigService):
|
|
|
107
118
|
payload=merged,
|
|
108
119
|
app=self.app_id.value,
|
|
109
120
|
)
|
|
121
|
+
# Track which project paths we own so a later source removal prunes only
|
|
122
|
+
# our `mcpServers` sub-keys without touching the user's project entries.
|
|
123
|
+
projects_scope = app_scope(self.app_id, "projects")
|
|
124
|
+
action.scope = projects_scope
|
|
125
|
+
action.managed_entries = {projects_scope: sorted(desired_keys)}
|
|
126
|
+
return action
|
|
110
127
|
|
|
111
128
|
def plan_skill_actions(
|
|
112
129
|
self,
|
|
@@ -16,6 +16,7 @@ from code_agnostic.apps.codex.config_repository import CodexConfigRepository
|
|
|
16
16
|
from code_agnostic.apps.codex.mapper import CodexMCPMapper
|
|
17
17
|
from code_agnostic.apps.codex.schema_repository import CodexSchemaRepository
|
|
18
18
|
from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
|
|
19
|
+
from code_agnostic.apps.common.utils import apply_mcp_servers
|
|
19
20
|
from code_agnostic.apps.common.interfaces.repositories import (
|
|
20
21
|
IAppConfigRepository,
|
|
21
22
|
ISchemaRepository,
|
|
@@ -118,6 +119,7 @@ class CodexConfigService(RegisteredAppConfigService):
|
|
|
118
119
|
common_servers: dict[str, MCPServerDTO],
|
|
119
120
|
agent_sources: list[Path] | None = None,
|
|
120
121
|
previously_managed: set[str] | None = None,
|
|
122
|
+
previously_managed_agents: set[str] | None = None,
|
|
121
123
|
*,
|
|
122
124
|
replace_mcp: bool = False,
|
|
123
125
|
) -> Action:
|
|
@@ -144,10 +146,17 @@ class CodexConfigService(RegisteredAppConfigService):
|
|
|
144
146
|
self.set_mcp_payload(
|
|
145
147
|
merged, desired_mcp, previously_managed, replace=replace_mcp
|
|
146
148
|
)
|
|
147
|
-
if agent_sources
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
self.
|
|
149
|
+
registry = self._build_agent_registry(agent_sources) if agent_sources else {}
|
|
150
|
+
if replace_mcp:
|
|
151
|
+
if registry:
|
|
152
|
+
merged["agents"] = self._merge_agents_payload(
|
|
153
|
+
merged.get("agents"), registry
|
|
154
|
+
)
|
|
155
|
+
elif registry or previously_managed_agents:
|
|
156
|
+
# Ownership-aware: prune agent-registry entries we previously wrote
|
|
157
|
+
# that are gone from the source, keep base-config/user agents.
|
|
158
|
+
merged["agents"] = apply_mcp_servers(
|
|
159
|
+
merged.get("agents"), registry, previously_managed_agents
|
|
151
160
|
)
|
|
152
161
|
self.validate_config(merged)
|
|
153
162
|
|
|
@@ -161,7 +170,10 @@ class CodexConfigService(RegisteredAppConfigService):
|
|
|
161
170
|
)
|
|
162
171
|
if not replace_mcp:
|
|
163
172
|
action.scope = app_scope(self.app_id, "mcp")
|
|
164
|
-
action.
|
|
173
|
+
action.managed_entries = {
|
|
174
|
+
app_scope(self.app_id, "mcp"): sorted(desired_mcp),
|
|
175
|
+
app_scope(self.app_id, "agents_registry"): sorted(registry),
|
|
176
|
+
}
|
|
165
177
|
return action
|
|
166
178
|
|
|
167
179
|
def _merge_agents_payload(
|
|
@@ -270,6 +270,7 @@ class IAppConfigService(ABC):
|
|
|
270
270
|
common_servers: dict[str, MCPServerDTO],
|
|
271
271
|
agent_sources: list[Path] | None = None,
|
|
272
272
|
previously_managed: set[str] | None = None,
|
|
273
|
+
previously_managed_agents: set[str] | None = None,
|
|
273
274
|
*,
|
|
274
275
|
replace_mcp: bool = False,
|
|
275
276
|
) -> Action:
|
|
@@ -296,7 +297,9 @@ class IAppConfigService(ABC):
|
|
|
296
297
|
# User-shared global config: track which servers we own so a later
|
|
297
298
|
# source removal can be pruned without touching the user's servers.
|
|
298
299
|
action.scope = app_scope(self.app_id, "mcp")
|
|
299
|
-
action.
|
|
300
|
+
action.managed_entries = {
|
|
301
|
+
app_scope(self.app_id, "mcp"): sorted(desired_mcp)
|
|
302
|
+
}
|
|
300
303
|
return action
|
|
301
304
|
|
|
302
305
|
def build_plan(
|
|
@@ -313,6 +316,9 @@ class IAppConfigService(ABC):
|
|
|
313
316
|
previously_managed_mcp = self._load_state_names(
|
|
314
317
|
managed_mcp_group, app_scope(self.app_id, "mcp")
|
|
315
318
|
)
|
|
319
|
+
previously_managed_agents = self._load_state_names(
|
|
320
|
+
managed_mcp_group, app_scope(self.app_id, "agents_registry")
|
|
321
|
+
)
|
|
316
322
|
|
|
317
323
|
skill_actions, skill_skipped = self._build_compiled_group(
|
|
318
324
|
sources=source_repository.list_skill_sources(),
|
|
@@ -341,6 +347,7 @@ class IAppConfigService(ABC):
|
|
|
341
347
|
common_servers,
|
|
342
348
|
agent_sources=source_repository.list_agent_sources(),
|
|
343
349
|
previously_managed=previously_managed_mcp,
|
|
350
|
+
previously_managed_agents=previously_managed_agents,
|
|
344
351
|
),
|
|
345
352
|
*skill_actions,
|
|
346
353
|
*agent_actions,
|
|
@@ -137,6 +137,7 @@ class OpenCodeConfigService(RegisteredAppConfigService):
|
|
|
137
137
|
common_servers: dict[str, MCPServerDTO],
|
|
138
138
|
agent_sources: list[Path] | None = None,
|
|
139
139
|
previously_managed: set[str] | None = None,
|
|
140
|
+
previously_managed_agents: set[str] | None = None,
|
|
140
141
|
*,
|
|
141
142
|
replace_mcp: bool = False,
|
|
142
143
|
) -> Action:
|
|
@@ -174,7 +175,9 @@ class OpenCodeConfigService(RegisteredAppConfigService):
|
|
|
174
175
|
)
|
|
175
176
|
if not replace_mcp:
|
|
176
177
|
action.scope = app_scope(self.app_id, "mcp")
|
|
177
|
-
action.
|
|
178
|
+
action.managed_entries = {
|
|
179
|
+
app_scope(self.app_id, "mcp"): sorted(desired_mcp)
|
|
180
|
+
}
|
|
178
181
|
return action
|
|
179
182
|
|
|
180
183
|
def _validate_common_mcp(self, common_servers: dict[str, MCPServerDTO]) -> None:
|
|
@@ -770,12 +770,14 @@ class SyncExecutor:
|
|
|
770
770
|
if action.scope is None:
|
|
771
771
|
continue
|
|
772
772
|
|
|
773
|
-
if action.
|
|
774
|
-
# User-shared
|
|
775
|
-
#
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
773
|
+
if action.managed_entries is not None:
|
|
774
|
+
# User-shared config: track the named entries we own (MCP servers,
|
|
775
|
+
# agent-registry names, …) by scope — never the shared config file
|
|
776
|
+
# itself, so cleanup prunes only what we wrote.
|
|
777
|
+
for entry_scope, names in action.managed_entries.items():
|
|
778
|
+
global_mcp_touched.add(entry_scope)
|
|
779
|
+
if names:
|
|
780
|
+
global_mcp[entry_scope] = sorted(set(names))
|
|
779
781
|
continue
|
|
780
782
|
|
|
781
783
|
if action.workspace is not None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
from code_agnostic.apps.app_id import AppId, app_metadata
|
|
3
|
+
from code_agnostic.apps.app_id import AppId, app_metadata, app_scope
|
|
4
4
|
from code_agnostic.apps.common.compiled_planning import (
|
|
5
5
|
plan_owned_compiled_text_action,
|
|
6
6
|
)
|
|
@@ -283,15 +283,29 @@ class SyncPlanner:
|
|
|
283
283
|
if existing_action is not None and isinstance(existing_action.payload, dict)
|
|
284
284
|
else None
|
|
285
285
|
)
|
|
286
|
+
state = self.core.load_state()
|
|
287
|
+
managed_mcp = state.get("managed_mcp")
|
|
288
|
+
if not isinstance(managed_mcp, dict):
|
|
289
|
+
managed_mcp = {}
|
|
290
|
+
prev_projects_raw = managed_mcp.get(app_scope(AppId.CLAUDE, "projects"), [])
|
|
291
|
+
previously_managed_projects = (
|
|
292
|
+
{path for path in prev_projects_raw if isinstance(path, str)}
|
|
293
|
+
if isinstance(prev_projects_raw, list)
|
|
294
|
+
else set()
|
|
295
|
+
)
|
|
286
296
|
project_action = claude_service.build_project_mcp_action(
|
|
287
297
|
self._claude_project_mcp,
|
|
288
298
|
base_payload=base_payload,
|
|
299
|
+
previously_managed_projects=previously_managed_projects,
|
|
289
300
|
)
|
|
290
301
|
if existing_action is not None:
|
|
291
302
|
# Carry global MCP ownership tracking onto the merged Claude action so
|
|
292
|
-
# top-level `mcpServers` orphans still get pruned on later applies.
|
|
293
|
-
|
|
294
|
-
project_action.
|
|
303
|
+
# top-level `mcpServers` orphans still get pruned on later applies. The
|
|
304
|
+
# project-path ownership set by build_project_mcp_action is preserved.
|
|
305
|
+
project_action.scope = existing_action.scope or project_action.scope
|
|
306
|
+
merged_entries = dict(existing_action.managed_entries or {})
|
|
307
|
+
merged_entries.update(project_action.managed_entries or {})
|
|
308
|
+
project_action.managed_entries = merged_entries
|
|
295
309
|
actions = [
|
|
296
310
|
action
|
|
297
311
|
for action in plan.actions
|
|
@@ -128,6 +128,7 @@ tests/test_cli_agents.py
|
|
|
128
128
|
tests/test_cli_aliases.py
|
|
129
129
|
tests/test_cli_apply_apps.py
|
|
130
130
|
tests/test_cli_apply_codex.py
|
|
131
|
+
tests/test_cli_apply_codex_agent_cleanup.py
|
|
131
132
|
tests/test_cli_apply_cursor.py
|
|
132
133
|
tests/test_cli_apply_mcp_cleanup.py
|
|
133
134
|
tests/test_cli_apply_target.py
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Codex agent-registry ownership: an agent removed from the source is pruned
|
|
2
|
+
from `[agents.*]` in ~/.codex/config.toml on the next apply, while base-config
|
|
3
|
+
agent settings and user-added entries survive."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from code_agnostic.__main__ import cli
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import tomllib
|
|
11
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
12
|
+
import tomli as tomllib # type: ignore
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _write_agent(core_root: Path, name: str) -> None:
|
|
16
|
+
agent = core_root / "agents" / f"{name}.md"
|
|
17
|
+
agent.parent.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
agent.write_text(
|
|
19
|
+
f"---\nname: {name}\ndescription: {name} specialist\n---\n\nDo {name}.\n",
|
|
20
|
+
encoding="utf-8",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_apply_codex_prunes_removed_agent_from_registry(
|
|
25
|
+
minimal_shared_config: Path,
|
|
26
|
+
core_root: Path,
|
|
27
|
+
tmp_path: Path,
|
|
28
|
+
cli_runner,
|
|
29
|
+
enable_app,
|
|
30
|
+
) -> None:
|
|
31
|
+
enable_app("codex")
|
|
32
|
+
# Base-config agent settings + a user-managed agent entry must be preserved.
|
|
33
|
+
(core_root / "config" / "codex.base.json").write_text(
|
|
34
|
+
'{"agents": {"max_depth": 2}}', encoding="utf-8"
|
|
35
|
+
)
|
|
36
|
+
(tmp_path / ".codex").mkdir(parents=True, exist_ok=True)
|
|
37
|
+
(tmp_path / ".codex" / "config.toml").write_text(
|
|
38
|
+
'[agents.mine]\ndescription = "user agent"\nconfig_file = "agents/mine.toml"\n',
|
|
39
|
+
encoding="utf-8",
|
|
40
|
+
)
|
|
41
|
+
_write_agent(core_root, "planner")
|
|
42
|
+
_write_agent(core_root, "reviewer")
|
|
43
|
+
|
|
44
|
+
assert cli_runner.invoke(cli, ["apply", "-a", "codex"]).exit_code == 0
|
|
45
|
+
cfg = tomllib.loads(
|
|
46
|
+
(tmp_path / ".codex" / "config.toml").read_text(encoding="utf-8")
|
|
47
|
+
)
|
|
48
|
+
assert cfg["agents"]["max_depth"] == 2
|
|
49
|
+
assert set(cfg["agents"]) >= {"planner", "reviewer", "mine", "max_depth"}
|
|
50
|
+
|
|
51
|
+
state_path = core_root / ".sync-state.json"
|
|
52
|
+
import json
|
|
53
|
+
|
|
54
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
55
|
+
assert state["managed_mcp"]["app:codex:agents_registry"] == ["planner", "reviewer"]
|
|
56
|
+
|
|
57
|
+
# Remove reviewer from the source and re-apply.
|
|
58
|
+
(core_root / "agents" / "reviewer.md").unlink()
|
|
59
|
+
assert cli_runner.invoke(cli, ["apply", "-a", "codex"]).exit_code == 0
|
|
60
|
+
|
|
61
|
+
cfg = tomllib.loads(
|
|
62
|
+
(tmp_path / ".codex" / "config.toml").read_text(encoding="utf-8")
|
|
63
|
+
)
|
|
64
|
+
agents = cfg["agents"]
|
|
65
|
+
assert "reviewer" not in agents, "our removed agent must be pruned from registry"
|
|
66
|
+
assert "planner" in agents
|
|
67
|
+
assert agents["mine"]["description"] == "user agent", "user entry preserved"
|
|
68
|
+
assert agents["max_depth"] == 2, "base-config agent settings preserved"
|
|
69
|
+
|
|
70
|
+
state = json.loads(state_path.read_text(encoding="utf-8"))
|
|
71
|
+
assert state["managed_mcp"]["app:codex:agents_registry"] == ["planner"]
|
|
@@ -6,6 +6,7 @@ import json
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from code_agnostic.__main__ import cli
|
|
9
|
+
from code_agnostic.core.repository import CoreRepository
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def _write_skill(core_root: Path, name: str) -> None:
|
|
@@ -67,3 +68,50 @@ def test_disable_cursor_removes_skill_and_prunes_owned_mcp(
|
|
|
67
68
|
# Flag is off.
|
|
68
69
|
apps_cfg = json.loads((core_root / "config" / "apps.json").read_text())
|
|
69
70
|
assert apps_cfg["cursor"] is False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_disable_claude_prunes_project_mcp_keeps_user_project(
|
|
74
|
+
minimal_shared_config: Path,
|
|
75
|
+
core_root: Path,
|
|
76
|
+
tmp_path: Path,
|
|
77
|
+
cli_runner,
|
|
78
|
+
enable_app,
|
|
79
|
+
) -> None:
|
|
80
|
+
enable_app("claude")
|
|
81
|
+
workspace_root = tmp_path / "workspace"
|
|
82
|
+
(workspace_root / "repo-a" / ".git").mkdir(parents=True)
|
|
83
|
+
|
|
84
|
+
core = CoreRepository(core_root)
|
|
85
|
+
core.add_workspace("myws", workspace_root)
|
|
86
|
+
(core.workspace_config_dir("myws") / "mcp.base.json").write_text(
|
|
87
|
+
json.dumps({"mcpServers": {"ws-server": {"url": "https://example.com/mcp"}}}),
|
|
88
|
+
encoding="utf-8",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# User's own project entry — must survive disable untouched.
|
|
92
|
+
config_path = tmp_path / ".claude.json"
|
|
93
|
+
user_project = str((tmp_path / "user-repo").resolve())
|
|
94
|
+
config_path.write_text(
|
|
95
|
+
json.dumps(
|
|
96
|
+
{"projects": {user_project: {"mcpServers": {"personal": {"type": "http"}}}}}
|
|
97
|
+
),
|
|
98
|
+
encoding="utf-8",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
assert cli_runner.invoke(cli, ["apply", "-a", "claude"]).exit_code == 0
|
|
102
|
+
|
|
103
|
+
projects = json.loads(config_path.read_text())["projects"]
|
|
104
|
+
repo_key = str((workspace_root / "repo-a").resolve())
|
|
105
|
+
assert "mcpServers" in projects[repo_key]
|
|
106
|
+
state = json.loads((core_root / ".sync-state.json").read_text())
|
|
107
|
+
assert repo_key in state["managed_mcp"]["app:claude:projects"]
|
|
108
|
+
|
|
109
|
+
# Disable → our project mcpServers pruned, user's project kept.
|
|
110
|
+
assert cli_runner.invoke(cli, ["apps", "disable", "-a", "claude"]).exit_code == 0
|
|
111
|
+
|
|
112
|
+
projects = json.loads(config_path.read_text())["projects"]
|
|
113
|
+
assert "mcpServers" not in projects.get(repo_key, {})
|
|
114
|
+
assert projects[user_project] == {"mcpServers": {"personal": {"type": "http"}}}
|
|
115
|
+
|
|
116
|
+
state = json.loads((core_root / ".sync-state.json").read_text())
|
|
117
|
+
assert "app:claude:projects" not in state.get("managed_mcp", {})
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Tests for workspace-level config sync (MCP, skills, agents, rules)."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
|
|
5
7
|
try:
|
|
@@ -747,6 +749,81 @@ def test_workspace_claude_mcp_merges_project_entries_into_home_config(
|
|
|
747
749
|
}
|
|
748
750
|
|
|
749
751
|
|
|
752
|
+
def test_workspace_claude_removed_repo_prunes_project_mcp_keeps_others(
|
|
753
|
+
minimal_shared_config: Path,
|
|
754
|
+
core_root: Path,
|
|
755
|
+
tmp_path: Path,
|
|
756
|
+
write_json,
|
|
757
|
+
) -> None:
|
|
758
|
+
"""P1: a repo removed from a workspace has its `projects.<path>.mcpServers`
|
|
759
|
+
pruned from ~/.claude.json on the next apply, while the other repo, the
|
|
760
|
+
workspace root, and a user-added project entry are all preserved."""
|
|
761
|
+
workspace_root = tmp_path / "workspace"
|
|
762
|
+
workspace_root.mkdir()
|
|
763
|
+
repo_a = workspace_root / "repo-a"
|
|
764
|
+
repo_b = workspace_root / "repo-b"
|
|
765
|
+
(repo_a / ".git").mkdir(parents=True)
|
|
766
|
+
(repo_b / ".git").mkdir(parents=True)
|
|
767
|
+
|
|
768
|
+
core = CoreRepository(core_root)
|
|
769
|
+
core.add_workspace("myws", workspace_root)
|
|
770
|
+
|
|
771
|
+
ws_config = core.workspace_config_dir("myws")
|
|
772
|
+
write_json(
|
|
773
|
+
ws_config / "mcp.base.json",
|
|
774
|
+
{"mcpServers": {"ws-server": {"url": "https://example.com/mcp"}}},
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
# User has their own project entry in ~/.claude.json that we must never touch.
|
|
778
|
+
user_project = str((tmp_path / "user-repo").resolve())
|
|
779
|
+
config_path = tmp_path / ".claude.json"
|
|
780
|
+
write_json(
|
|
781
|
+
config_path,
|
|
782
|
+
{"projects": {user_project: {"mcpServers": {"personal": {"type": "http"}}}}},
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
def _apply() -> dict:
|
|
786
|
+
plan = SyncPlanner(
|
|
787
|
+
core=core,
|
|
788
|
+
app_services=[_claude_service(tmp_path / ".claude")],
|
|
789
|
+
).build()
|
|
790
|
+
_applied, failed, failures = SyncExecutor(core=core).execute(plan)
|
|
791
|
+
assert failed == 0, failures
|
|
792
|
+
return json.loads(config_path.read_text(encoding="utf-8"))
|
|
793
|
+
|
|
794
|
+
payload = _apply()
|
|
795
|
+
projects = payload["projects"]
|
|
796
|
+
assert str(workspace_root.resolve()) in projects
|
|
797
|
+
assert "mcpServers" in projects[str(repo_a.resolve())]
|
|
798
|
+
assert "mcpServers" in projects[str(repo_b.resolve())]
|
|
799
|
+
assert user_project in projects
|
|
800
|
+
|
|
801
|
+
state = json.loads((core_root / ".sync-state.json").read_text(encoding="utf-8"))
|
|
802
|
+
assert set(state["managed_mcp"]["app:claude:projects"]) == {
|
|
803
|
+
str(workspace_root.resolve()),
|
|
804
|
+
str(repo_a.resolve()),
|
|
805
|
+
str(repo_b.resolve()),
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
# Remove repo-a from the workspace and re-apply.
|
|
809
|
+
shutil.rmtree(repo_a)
|
|
810
|
+
payload = _apply()
|
|
811
|
+
projects = payload["projects"]
|
|
812
|
+
|
|
813
|
+
assert "mcpServers" not in projects.get(
|
|
814
|
+
str(repo_a.resolve()), {}
|
|
815
|
+
), "removed repo's mcpServers must be pruned"
|
|
816
|
+
assert "mcpServers" in projects[str(repo_b.resolve())]
|
|
817
|
+
assert "mcpServers" in projects[str(workspace_root.resolve())]
|
|
818
|
+
assert projects[user_project] == {"mcpServers": {"personal": {"type": "http"}}}
|
|
819
|
+
|
|
820
|
+
state = json.loads((core_root / ".sync-state.json").read_text(encoding="utf-8"))
|
|
821
|
+
assert set(state["managed_mcp"]["app:claude:projects"]) == {
|
|
822
|
+
str(workspace_root.resolve()),
|
|
823
|
+
str(repo_b.resolve()),
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
|
|
750
827
|
def test_workspace_targeted_plan_does_not_cleanup_other_app_scopes(
|
|
751
828
|
minimal_shared_config: Path,
|
|
752
829
|
core_root: Path,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_agnostic-0.5.1 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|