code-agnostic 0.3.17__tar.gz → 0.4.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.3.17 → code_agnostic-0.4.0}/PKG-INFO +4 -1
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/README.md +3 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/__init__.py +1 -1
- code_agnostic-0.4.0/code_agnostic/apps/apps_service.py +294 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/claude/service.py +4 -10
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/codex/schema.json +325 -50
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/codex/service.py +16 -15
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/common/interfaces/service.py +45 -5
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/common/utils.py +36 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/copilot/service.py +4 -5
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/cursor/service.py +4 -5
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/opencode/config_repository.py +11 -2
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/opencode/service.py +21 -9
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/apply.py +8 -2
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/apps.py +7 -2
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/plan.py +14 -3
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/core/repository.py +4 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/executor.py +16 -1
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/models.py +1 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/planner.py +33 -23
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/tui/renderers.py +13 -4
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic.egg-info/PKG-INFO +4 -1
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic.egg-info/SOURCES.txt +2 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/pyproject.toml +1 -1
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_apply_apps.py +37 -3
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_apply_codex.py +4 -2
- code_agnostic-0.4.0/tests/test_cli_apply_mcp_cleanup.py +102 -0
- code_agnostic-0.4.0/tests/test_cli_disable_cleanup.py +69 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_plan.py +12 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_common_repository.py +2 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_project_config_sync.py +7 -1
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_workspace_config_sync.py +5 -1
- code_agnostic-0.3.17/code_agnostic/apps/apps_service.py +0 -138
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/LICENSE +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/__main__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/agents/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/agents/claude.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/agents/codex.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/agents/compilers.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/agents/copilot.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/agents/models.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/agents/opencode.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/agents/parser.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/app_id.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/claude/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/claude/config_repository.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/claude/mapper.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/codex/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/codex/config_repository.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/codex/mapper.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/codex/schema_repository.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/common/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/common/compiled_planning.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/common/framework.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/common/loader.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/common/models.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/common/schema.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/common/symlink_planning.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/copilot/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/copilot/config_repository.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/copilot/mapper.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/cursor/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/cursor/config_repository.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/cursor/mapper.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/cursor/schema.json +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/cursor/schema_repository.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/opencode/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/opencode/mapper.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/opencode/schema.json +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/apps/opencode/schema_repository.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/aliases.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/agents.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/explain_lossiness.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/import_.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/mcp.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/projects.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/restore.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/rules.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/skills.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/status.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/validate.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/commands/workspaces.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/helpers.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/cli/options.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/constants.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/core/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/core/project_repository.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/core/workspace_repository.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/errors.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/generated_artifacts.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/git_exclude_service.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/imports/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/imports/adapters.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/imports/filesystem.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/imports/models.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/imports/service.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/lossiness.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/mcp_service.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/project_artifacts.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/rules/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/rules/compilers.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/rules/models.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/rules/parser.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/rules/repository.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/skills/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/skills/compilers.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/skills/install_sources.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/skills/models.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/skills/parser.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/spec/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/spec/loaders.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/status.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/tui/__init__.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/tui/enums.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/tui/import_selector.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/tui/sections.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/tui/tables.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/utils.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/validation.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/workspace_artifacts.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic/workspaces.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic.egg-info/dependency_links.txt +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic.egg-info/entry_points.txt +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic.egg-info/requires.txt +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/code_agnostic.egg-info/top_level.txt +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/setup.cfg +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_agents.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_aliases.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_apply_cursor.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_apply_target.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_apps.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_explain_lossiness.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_flags.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_git_exclude.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_import.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_import_interactive.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_mcp.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_module_organization.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_projects.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_restore.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_rules.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_skills.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_status.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_validate.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_workspace_resolution.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_cli_workspaces.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_common_mcp_to_dto.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_compiled_planning.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_dto_to_common_mcp.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_git_exclude_service.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_mcp_service.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_planner_executor.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_planner_rules.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_skill_install_sources.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_symlink_planning.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_sync_plan_model.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_transactional_executor.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_utils.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_version.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_workspace_repo_status.py +0 -0
- {code_agnostic-0.3.17 → code_agnostic-0.4.0}/tests/test_workspaces.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-agnostic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Centralized hub for LLM coding config: MCP, skills, rules, and agents.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -305,7 +305,10 @@ code-agnostic workspaces list
|
|
|
305
305
|
|
|
306
306
|
Prevent synced paths from showing up in `git status`. Managed per-workspace with customizable patterns.
|
|
307
307
|
|
|
308
|
+
Writing `.git/info/exclude` is always explicit — `apply` does **not** touch it by default. Opt in per run with `apply --apply-excludes`, or use the standalone command:
|
|
309
|
+
|
|
308
310
|
```bash
|
|
311
|
+
code-agnostic apply --apply-excludes # apply + write excludes
|
|
309
312
|
code-agnostic workspaces git-exclude # all workspaces
|
|
310
313
|
code-agnostic workspaces git-exclude -w myproject # one workspace
|
|
311
314
|
code-agnostic workspaces exclude-add --pattern "*.generated" -w myproject
|
|
@@ -280,7 +280,10 @@ code-agnostic workspaces list
|
|
|
280
280
|
|
|
281
281
|
Prevent synced paths from showing up in `git status`. Managed per-workspace with customizable patterns.
|
|
282
282
|
|
|
283
|
+
Writing `.git/info/exclude` is always explicit — `apply` does **not** touch it by default. Opt in per run with `apply --apply-excludes`, or use the standalone command:
|
|
284
|
+
|
|
283
285
|
```bash
|
|
286
|
+
code-agnostic apply --apply-excludes # apply + write excludes
|
|
284
287
|
code-agnostic workspaces git-exclude # all workspaces
|
|
285
288
|
code-agnostic workspaces git-exclude -w myproject # one workspace
|
|
286
289
|
code-agnostic workspaces exclude-add --pattern "*.generated" -w myproject
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from code_agnostic.apps.app_id import AppId, app_ids_by_capability, app_scope
|
|
5
|
+
from code_agnostic.apps.common.framework import (
|
|
6
|
+
create_registered_app_service,
|
|
7
|
+
list_registered_app_services,
|
|
8
|
+
)
|
|
9
|
+
from code_agnostic.apps.common.interfaces.service import IAppConfigService
|
|
10
|
+
from code_agnostic.apps.common.interfaces.repositories import ISourceRepository
|
|
11
|
+
from code_agnostic.apps.common.symlink_planning import (
|
|
12
|
+
load_state_links,
|
|
13
|
+
load_state_paths,
|
|
14
|
+
plan_stale_files_group,
|
|
15
|
+
plan_stale_group,
|
|
16
|
+
)
|
|
17
|
+
from code_agnostic.core.project_repository import ProjectConfigRepository
|
|
18
|
+
from code_agnostic.core.workspace_repository import WorkspaceConfigRepository
|
|
19
|
+
from code_agnostic.executor import SyncExecutor
|
|
20
|
+
from code_agnostic.models import Action, AppStatusRow, AppSyncStatus, SyncPlan
|
|
21
|
+
from code_agnostic.planner import SyncPlanner
|
|
22
|
+
from code_agnostic.project_artifacts import load_project_entries, project_config_dir
|
|
23
|
+
from code_agnostic.utils import read_json_safe, write_json
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AppsService:
|
|
27
|
+
def __init__(self, core_repository: ISourceRepository) -> None:
|
|
28
|
+
self.core_repository = core_repository
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def apps_path(self) -> Path:
|
|
32
|
+
return self.core_repository.root / "config" / "apps.json"
|
|
33
|
+
|
|
34
|
+
def available_apps(self) -> list[str]:
|
|
35
|
+
registered = set(list_registered_app_services())
|
|
36
|
+
manageable = set(app_ids_by_capability(toggleable=True))
|
|
37
|
+
return [
|
|
38
|
+
app.value
|
|
39
|
+
for app in sorted(registered & manageable, key=lambda item: item.value)
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
def load_apps(self) -> dict[str, bool]:
|
|
43
|
+
payload, error = read_json_safe(self.apps_path)
|
|
44
|
+
if error is not None or not isinstance(payload, dict):
|
|
45
|
+
return self._default_apps()
|
|
46
|
+
|
|
47
|
+
result = self._default_apps()
|
|
48
|
+
for app_name in self.available_apps():
|
|
49
|
+
value = payload.get(app_name)
|
|
50
|
+
if isinstance(value, bool):
|
|
51
|
+
result[app_name] = value
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
def save_apps(self, apps: dict[str, bool]) -> None:
|
|
55
|
+
normalized = self._default_apps()
|
|
56
|
+
for app_name in self.available_apps():
|
|
57
|
+
value = apps.get(app_name)
|
|
58
|
+
if isinstance(value, bool):
|
|
59
|
+
normalized[app_name] = value
|
|
60
|
+
write_json(self.apps_path, normalized)
|
|
61
|
+
|
|
62
|
+
def is_enabled(self, app_name: str) -> bool:
|
|
63
|
+
return self.load_apps().get(app_name, False)
|
|
64
|
+
|
|
65
|
+
def set_enabled(self, app_name: str, enabled: bool) -> None:
|
|
66
|
+
if app_name not in self.available_apps():
|
|
67
|
+
raise ValueError(f"Unknown app: {app_name}")
|
|
68
|
+
apps = self.load_apps()
|
|
69
|
+
apps[app_name] = enabled
|
|
70
|
+
self.save_apps(apps)
|
|
71
|
+
|
|
72
|
+
def enable(self, app_name: str) -> None:
|
|
73
|
+
self.set_enabled(app_name=app_name, enabled=True)
|
|
74
|
+
|
|
75
|
+
def disable(self, app_name: str) -> tuple[int, int, list[str]]:
|
|
76
|
+
"""Disable an app AND clean up everything it previously synced.
|
|
77
|
+
|
|
78
|
+
Removes tracked skills/agents/compiled files and prunes the MCP servers
|
|
79
|
+
we own from the app's shared config, across global, workspace, and
|
|
80
|
+
project scopes, then clears the corresponding state. Without this,
|
|
81
|
+
disabling would strand orphaned artifacts with no way to reclaim them.
|
|
82
|
+
"""
|
|
83
|
+
normalized = app_name.lower()
|
|
84
|
+
plan = self._plan_app_cleanup(normalized)
|
|
85
|
+
result: tuple[int, int, list[str]] = (0, 0, [])
|
|
86
|
+
if plan.actions:
|
|
87
|
+
result = SyncExecutor(core=self.core_repository).execute(
|
|
88
|
+
plan, persist_state=True
|
|
89
|
+
)
|
|
90
|
+
self.set_enabled(app_name=normalized, enabled=False)
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
def _plan_app_cleanup(self, app_name: str) -> SyncPlan:
|
|
94
|
+
try:
|
|
95
|
+
app_id = AppId(app_name)
|
|
96
|
+
except ValueError:
|
|
97
|
+
return SyncPlan([], [], [])
|
|
98
|
+
|
|
99
|
+
actions: list[Action] = []
|
|
100
|
+
skipped: list[str] = []
|
|
101
|
+
core = self.core_repository
|
|
102
|
+
|
|
103
|
+
# --- Global scopes (app:<app>:*) ---
|
|
104
|
+
global_state = core.load_state()
|
|
105
|
+
global_links = self._normalize_group(global_state.get("managed_links"))
|
|
106
|
+
global_paths = self._normalize_group(global_state.get("managed_paths"))
|
|
107
|
+
global_mcp = self._normalize_group(global_state.get("managed_mcp"))
|
|
108
|
+
global_prefix = f"app:{app_id.value}:"
|
|
109
|
+
for scope in sorted(s for s in global_links if s.startswith(global_prefix)):
|
|
110
|
+
actions.extend(self._cleanup_links(global_links, scope, app_id.value))
|
|
111
|
+
for scope in sorted(s for s in global_paths if s.startswith(global_prefix)):
|
|
112
|
+
actions.extend(
|
|
113
|
+
self._cleanup_paths(global_paths, scope, app_id.value, skipped)
|
|
114
|
+
)
|
|
115
|
+
actions.extend(self._cleanup_global_mcp(app_id, global_mcp))
|
|
116
|
+
|
|
117
|
+
# --- Workspace scopes (ws:<app>:*) ---
|
|
118
|
+
ws_prefix = f"ws:{app_id.value}:"
|
|
119
|
+
for workspace in core.load_workspaces():
|
|
120
|
+
ws_name = workspace["name"]
|
|
121
|
+
ws_repo = WorkspaceConfigRepository(root=core.workspace_config_dir(ws_name))
|
|
122
|
+
ws_state = ws_repo.load_state()
|
|
123
|
+
ws_links = self._normalize_group(ws_state.get("managed_links"))
|
|
124
|
+
ws_paths = self._normalize_group(ws_state.get("managed_paths"))
|
|
125
|
+
for scope in sorted(s for s in ws_links if s.startswith(ws_prefix)):
|
|
126
|
+
for action in self._cleanup_links(ws_links, scope, "workspace"):
|
|
127
|
+
action.workspace = ws_name
|
|
128
|
+
actions.append(action)
|
|
129
|
+
for scope in sorted(s for s in ws_paths if s.startswith(ws_prefix)):
|
|
130
|
+
for action in self._cleanup_paths(
|
|
131
|
+
ws_paths, scope, "workspace", skipped
|
|
132
|
+
):
|
|
133
|
+
action.workspace = ws_name
|
|
134
|
+
actions.append(action)
|
|
135
|
+
|
|
136
|
+
# --- Project scopes (project:<app>:*) ---
|
|
137
|
+
project_prefix = f"project:{app_id.value}:"
|
|
138
|
+
for project in load_project_entries(core):
|
|
139
|
+
project_name = project["name"]
|
|
140
|
+
project_repo = ProjectConfigRepository(
|
|
141
|
+
root=project_config_dir(core, project_name)
|
|
142
|
+
)
|
|
143
|
+
project_state = project_repo.load_state()
|
|
144
|
+
project_links = self._normalize_group(project_state.get("managed_links"))
|
|
145
|
+
project_paths = self._normalize_group(project_state.get("managed_paths"))
|
|
146
|
+
for scope in sorted(
|
|
147
|
+
s for s in project_links if s.startswith(project_prefix)
|
|
148
|
+
):
|
|
149
|
+
for action in self._cleanup_links(project_links, scope, app_id.value):
|
|
150
|
+
action.project = project_name
|
|
151
|
+
actions.append(action)
|
|
152
|
+
for scope in sorted(
|
|
153
|
+
s for s in project_paths if s.startswith(project_prefix)
|
|
154
|
+
):
|
|
155
|
+
for action in self._cleanup_paths(
|
|
156
|
+
project_paths, scope, app_id.value, skipped
|
|
157
|
+
):
|
|
158
|
+
action.project = project_name
|
|
159
|
+
actions.append(action)
|
|
160
|
+
|
|
161
|
+
return SyncPlan(actions=actions, errors=[], skipped=skipped)
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _normalize_group(value: Any) -> dict[str, Any]:
|
|
165
|
+
return (
|
|
166
|
+
{scope: paths for scope, paths in value.items() if isinstance(scope, str)}
|
|
167
|
+
if isinstance(value, dict)
|
|
168
|
+
else {}
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _cleanup_links(group: dict[str, Any], scope: str, app: str) -> list[Action]:
|
|
173
|
+
return plan_stale_group(
|
|
174
|
+
old_links=load_state_links(group, scope),
|
|
175
|
+
desired_links=[],
|
|
176
|
+
remove_detail=f"remove {scope} symlink on disable",
|
|
177
|
+
conflict_detail="managed path is not a symlink",
|
|
178
|
+
noop_detail="managed symlink already absent",
|
|
179
|
+
app=app,
|
|
180
|
+
scope=scope,
|
|
181
|
+
skipped=[],
|
|
182
|
+
skipped_message="Disable cleanup skipped (not symlink): {path}",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def _cleanup_paths(
|
|
187
|
+
group: dict[str, Any], scope: str, app: str, skipped: list[str]
|
|
188
|
+
) -> list[Action]:
|
|
189
|
+
return plan_stale_files_group(
|
|
190
|
+
old_paths=load_state_paths(group, scope),
|
|
191
|
+
desired_paths=[],
|
|
192
|
+
remove_detail=f"remove {scope} file on disable",
|
|
193
|
+
conflict_detail="managed path is not a file",
|
|
194
|
+
noop_detail="managed file already absent",
|
|
195
|
+
app=app,
|
|
196
|
+
scope=scope,
|
|
197
|
+
skipped=skipped,
|
|
198
|
+
skipped_message="Disable cleanup skipped (not file): {path}",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def _cleanup_global_mcp(
|
|
202
|
+
self, app_id: AppId, managed_mcp: dict[str, Any]
|
|
203
|
+
) -> list[Action]:
|
|
204
|
+
scope = app_scope(app_id, "mcp")
|
|
205
|
+
raw = managed_mcp.get(scope, [])
|
|
206
|
+
managed = (
|
|
207
|
+
{item for item in raw if isinstance(item, str)}
|
|
208
|
+
if isinstance(raw, list)
|
|
209
|
+
else set()
|
|
210
|
+
)
|
|
211
|
+
if not managed:
|
|
212
|
+
return []
|
|
213
|
+
try:
|
|
214
|
+
service = create_registered_app_service(app_id)
|
|
215
|
+
except (KeyError, ValueError):
|
|
216
|
+
return []
|
|
217
|
+
if not service.repository.config_path.exists():
|
|
218
|
+
return []
|
|
219
|
+
# Empty desired + our previously-managed names → prune only our servers,
|
|
220
|
+
# keep the user's, and clear managed_mcp state (mcp_managed becomes []).
|
|
221
|
+
return [service.build_action({}, previously_managed=managed)]
|
|
222
|
+
|
|
223
|
+
def list_status_rows(self) -> list[AppStatusRow]:
|
|
224
|
+
apps = self.load_apps()
|
|
225
|
+
rows: list[AppStatusRow] = []
|
|
226
|
+
for app_name in self.available_apps():
|
|
227
|
+
enabled = apps.get(app_name, False)
|
|
228
|
+
detail = "enabled by user" if enabled else "disabled by default"
|
|
229
|
+
rows.append(
|
|
230
|
+
AppStatusRow(
|
|
231
|
+
name=app_name,
|
|
232
|
+
status=AppSyncStatus.ENABLED if enabled else AppSyncStatus.DISABLED,
|
|
233
|
+
detail=detail,
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
return rows
|
|
237
|
+
|
|
238
|
+
def enabled_apps(self) -> list[str]:
|
|
239
|
+
apps = self.load_apps()
|
|
240
|
+
return [name for name in self.available_apps() if apps.get(name, False)]
|
|
241
|
+
|
|
242
|
+
def plan_for_target(self, target: str, *, apply_excludes: bool = False) -> SyncPlan:
|
|
243
|
+
normalized = target.lower()
|
|
244
|
+
if normalized != "all" and not self.is_enabled(normalized):
|
|
245
|
+
return SyncPlan([], [], [f"{normalized} is disabled for sync."])
|
|
246
|
+
|
|
247
|
+
app_services = self._resolve_services_for_target(normalized)
|
|
248
|
+
plan = SyncPlanner(
|
|
249
|
+
core=self.core_repository,
|
|
250
|
+
app_services=app_services,
|
|
251
|
+
include_workspace=True,
|
|
252
|
+
include_git_excludes=apply_excludes,
|
|
253
|
+
).build()
|
|
254
|
+
if normalized == "all":
|
|
255
|
+
if (
|
|
256
|
+
not app_services
|
|
257
|
+
and not plan.actions
|
|
258
|
+
and not plan.errors
|
|
259
|
+
and not plan.skipped
|
|
260
|
+
):
|
|
261
|
+
return SyncPlan([], [], ["No apps enabled for sync."])
|
|
262
|
+
return plan
|
|
263
|
+
return plan.filter_for_target(normalized)
|
|
264
|
+
|
|
265
|
+
def execute_plan(self, scoped_plan: SyncPlan) -> tuple[int, int, list[str]]:
|
|
266
|
+
persist_state = self._requires_state_persist(scoped_plan)
|
|
267
|
+
return SyncExecutor(core=self.core_repository).execute(
|
|
268
|
+
scoped_plan, persist_state=persist_state
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def _resolve_services_for_target(self, target: str) -> list[IAppConfigService]:
|
|
272
|
+
enabled = set(self.enabled_apps())
|
|
273
|
+
if target == "all":
|
|
274
|
+
selected = enabled
|
|
275
|
+
else:
|
|
276
|
+
selected = {target} if target in enabled else set()
|
|
277
|
+
|
|
278
|
+
services: list[IAppConfigService] = []
|
|
279
|
+
for app in sorted(selected):
|
|
280
|
+
try:
|
|
281
|
+
services.append(create_registered_app_service(AppId(app)))
|
|
282
|
+
except (KeyError, ValueError):
|
|
283
|
+
continue
|
|
284
|
+
return services
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def _requires_state_persist(scoped_plan: SyncPlan) -> bool:
|
|
288
|
+
return any(
|
|
289
|
+
action.kind.value in ("symlink", "remove_symlink") or action.app is not None
|
|
290
|
+
for action in scoped_plan.actions
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _default_apps(self) -> dict[str, bool]:
|
|
294
|
+
return {name: False for name in self.available_apps()}
|
|
@@ -53,6 +53,10 @@ class ClaudeConfigService(RegisteredAppConfigService):
|
|
|
53
53
|
def mapper(self) -> IAppMCPMapper:
|
|
54
54
|
return self._mapper
|
|
55
55
|
|
|
56
|
+
@property
|
|
57
|
+
def mcp_config_key(self) -> str:
|
|
58
|
+
return "mcpServers"
|
|
59
|
+
|
|
56
60
|
def validate_config(self, payload: Any) -> None:
|
|
57
61
|
if not isinstance(payload, dict):
|
|
58
62
|
raise InvalidConfigSchemaError(
|
|
@@ -62,16 +66,6 @@ class ClaudeConfigService(RegisteredAppConfigService):
|
|
|
62
66
|
def build_action_payload(self, payload: dict[str, Any]) -> Any:
|
|
63
67
|
return payload
|
|
64
68
|
|
|
65
|
-
def set_mcp_payload(
|
|
66
|
-
self, merged: dict[str, Any], desired_mcp: dict[str, Any]
|
|
67
|
-
) -> None:
|
|
68
|
-
existing_mcp = merged.get("mcpServers")
|
|
69
|
-
preserved = deepcopy(existing_mcp) if isinstance(existing_mcp, dict) else {}
|
|
70
|
-
for name, config in desired_mcp.items():
|
|
71
|
-
preserved[name] = deepcopy(config)
|
|
72
|
-
if preserved:
|
|
73
|
-
merged["mcpServers"] = preserved
|
|
74
|
-
|
|
75
69
|
def derive_status(
|
|
76
70
|
self, existing: dict[str, Any], merged: dict[str, Any]
|
|
77
71
|
) -> ActionStatus:
|