code-agnostic 0.6.0__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.6.0 → code_agnostic-0.7.0}/PKG-INFO +1 -1
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/__init__.py +1 -1
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/apps_service.py +24 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/claude/service.py +19 -2
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/planner.py +18 -4
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic.egg-info/PKG-INFO +1 -1
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/pyproject.toml +1 -1
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_disable_cleanup.py +48 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_workspace_config_sync.py +77 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/LICENSE +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/README.md +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/__main__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/claude.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/codex.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/compilers.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/copilot.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/models.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/opencode.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/agents/parser.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/app_id.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/claude/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/claude/config_repository.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/claude/mapper.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/codex/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/codex/config_repository.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/codex/mapper.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/codex/schema.json +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/codex/schema_repository.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/codex/service.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/compiled_planning.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/framework.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/service.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/loader.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/models.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/schema.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/symlink_planning.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/utils.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/copilot/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/copilot/config_repository.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/copilot/mapper.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/copilot/service.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/config_repository.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/mapper.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/schema.json +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/schema_repository.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/cursor/service.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/config_repository.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/mapper.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/schema.json +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/schema_repository.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/service.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/aliases.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/agents.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/apply.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/apps.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/import_.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/mcp.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/plan.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/projects.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/restore.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/rules.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/skills.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/status.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/validate.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/commands/workspaces.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/helpers.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/cli/options.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/constants.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/core/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/core/project_repository.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/core/repository.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/core/workspace_repository.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/errors.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/executor.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/generated_artifacts.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/git_exclude_service.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/imports/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/imports/adapters.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/imports/filesystem.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/imports/models.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/imports/service.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/lossiness.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/mcp_service.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/models.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/project_artifacts.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/rules/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/rules/compilers.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/rules/models.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/rules/parser.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/rules/repository.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/skills/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/skills/compilers.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/skills/install_sources.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/skills/models.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/skills/parser.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/spec/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/spec/loaders.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/status.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/tui/__init__.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/tui/enums.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/tui/import_selector.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/tui/renderers.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/tui/sections.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/tui/tables.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/utils.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/validation.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/workspace_artifacts.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/workspaces.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic.egg-info/SOURCES.txt +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic.egg-info/dependency_links.txt +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic.egg-info/entry_points.txt +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic.egg-info/requires.txt +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic.egg-info/top_level.txt +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/setup.cfg +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_agents.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_aliases.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_apply_apps.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_apply_codex.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_apply_codex_agent_cleanup.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_apply_cursor.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_apply_mcp_cleanup.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_apply_target.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_apps.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_explain_lossiness.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_flags.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_git_exclude.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_import.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_import_interactive.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_mcp.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_module_organization.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_plan.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_projects.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_restore.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_rules.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_skills.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_status.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_validate.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_workspace_resolution.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_cli_workspaces.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_common_mcp_to_dto.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_common_repository.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_compiled_planning.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_dto_to_common_mcp.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_git_exclude_service.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_mcp_service.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_planner_executor.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_planner_rules.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_project_config_sync.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_skill_install_sources.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_symlink_planning.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_sync_plan_model.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_transactional_executor.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_utils.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_version.py +0 -0
- {code_agnostic-0.6.0 → code_agnostic-0.7.0}/tests/test_workspace_repo_status.py +0 -0
- {code_agnostic-0.6.0 → 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}:"
|
|
@@ -224,6 +227,27 @@ class AppsService:
|
|
|
224
227
|
)
|
|
225
228
|
]
|
|
226
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
|
+
|
|
227
251
|
@staticmethod
|
|
228
252
|
def _names_for_scope(group: dict[str, Any], scope: str) -> set[str]:
|
|
229
253
|
raw = group.get(scope, [])
|
|
@@ -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,
|
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
{code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/common/interfaces/repositories.py
RENAMED
|
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.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/config_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_agnostic-0.6.0 → code_agnostic-0.7.0}/code_agnostic/apps/opencode/schema_repository.py
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|