code-agnostic 0.3.2__tar.gz → 0.3.4__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.2 → code_agnostic-0.3.4}/PKG-INFO +5 -5
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/README.md +4 -4
- code_agnostic-0.3.4/code_agnostic/__main__.py +62 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/app_id.py +22 -4
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/codex/config_repository.py +10 -4
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/codex/schema.json +45 -1
- code_agnostic-0.3.4/code_agnostic/apps/codex/service.py +274 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/compiled_planning.py +12 -0
- code_agnostic-0.3.4/code_agnostic/apps/common/interfaces/service.py +284 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/utils.py +46 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/config_repository.py +10 -4
- code_agnostic-0.3.4/code_agnostic/apps/cursor/service.py +164 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/config_repository.py +13 -6
- code_agnostic-0.3.4/code_agnostic/apps/opencode/service.py +236 -0
- code_agnostic-0.3.4/code_agnostic/cli/__init__.py +29 -0
- code_agnostic-0.3.4/code_agnostic/cli/aliases.py +20 -0
- code_agnostic-0.3.4/code_agnostic/cli/commands/__init__.py +33 -0
- code_agnostic-0.3.4/code_agnostic/cli/commands/agents.py +47 -0
- code_agnostic-0.3.4/code_agnostic/cli/commands/apply.py +42 -0
- code_agnostic-0.3.4/code_agnostic/cli/commands/apps.py +45 -0
- code_agnostic-0.3.4/code_agnostic/cli/commands/explain_lossiness.py +45 -0
- code_agnostic-0.3.4/code_agnostic/cli/commands/import_.py +189 -0
- code_agnostic-0.3.4/code_agnostic/cli/commands/mcp.py +122 -0
- code_agnostic-0.3.4/code_agnostic/cli/commands/plan.py +30 -0
- code_agnostic-0.3.4/code_agnostic/cli/commands/restore.py +28 -0
- code_agnostic-0.3.4/code_agnostic/cli/commands/rules.py +46 -0
- code_agnostic-0.3.4/code_agnostic/cli/commands/skills.py +42 -0
- code_agnostic-0.3.4/code_agnostic/cli/commands/status.py +52 -0
- code_agnostic-0.3.4/code_agnostic/cli/commands/validate.py +30 -0
- code_agnostic-0.3.4/code_agnostic/cli/commands/workspaces.py +186 -0
- code_agnostic-0.3.4/code_agnostic/cli/helpers.py +87 -0
- code_agnostic-0.3.4/code_agnostic/cli/options.py +95 -0
- code_agnostic-0.3.4/code_agnostic/constants.py +32 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/core/repository.py +4 -3
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/executor.py +24 -13
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/git_exclude_service.py +6 -1
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/planner.py +139 -90
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/status.py +15 -19
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/utils.py +14 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic.egg-info/PKG-INFO +5 -5
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic.egg-info/SOURCES.txt +20 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/pyproject.toml +1 -1
- code_agnostic-0.3.4/tests/test_cli_apply_apps.py +574 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_apply_codex.py +14 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_apply_target.py +51 -0
- code_agnostic-0.3.4/tests/test_cli_module_organization.py +55 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_workspaces.py +7 -4
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_common_mcp_to_dto.py +64 -1
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_git_exclude_service.py +8 -3
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_planner_executor.py +40 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_planner_rules.py +1 -2
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_transactional_executor.py +51 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_workspace_config_sync.py +315 -27
- code_agnostic-0.3.4/tests/test_workspace_repo_status.py +240 -0
- code_agnostic-0.3.2/code_agnostic/__main__.py +0 -1071
- code_agnostic-0.3.2/code_agnostic/apps/codex/service.py +0 -365
- code_agnostic-0.3.2/code_agnostic/apps/common/interfaces/service.py +0 -84
- code_agnostic-0.3.2/code_agnostic/apps/cursor/service.py +0 -291
- code_agnostic-0.3.2/code_agnostic/apps/opencode/service.py +0 -353
- code_agnostic-0.3.2/code_agnostic/constants.py +0 -13
- code_agnostic-0.3.2/tests/test_cli_apply_apps.py +0 -256
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/LICENSE +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/agents/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/agents/codex.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/agents/compilers.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/agents/models.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/agents/opencode.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/agents/parser.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/apps_service.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/codex/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/codex/mapper.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/codex/schema_repository.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/framework.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/interfaces/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/interfaces/mapper.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/interfaces/repositories.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/loader.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/models.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/schema.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/common/symlink_planning.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/mapper.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/schema.json +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/cursor/schema_repository.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/mapper.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/schema.json +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/apps/opencode/schema_repository.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/core/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/core/workspace_repository.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/errors.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/imports/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/imports/adapters.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/imports/filesystem.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/imports/models.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/imports/service.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/lossiness.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/mcp_service.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/models.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/rules/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/rules/compilers.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/rules/models.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/rules/parser.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/rules/repository.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/skills/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/skills/compilers.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/skills/models.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/skills/parser.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/spec/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/spec/loaders.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/agent.v1.schema.json +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/mcp.base.schema.json +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/mcp.v1.schema.json +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/rule.v1.schema.json +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/spec/schemas/skill.v1.schema.json +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/tui/__init__.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/tui/enums.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/tui/import_selector.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/tui/renderers.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/tui/sections.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/tui/tables.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/validation.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic/workspaces.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic.egg-info/dependency_links.txt +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic.egg-info/entry_points.txt +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic.egg-info/requires.txt +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/code_agnostic.egg-info/top_level.txt +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/setup.cfg +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_agents.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_aliases.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_apply_cursor.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_apps.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_explain_lossiness.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_flags.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_git_exclude.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_import.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_import_interactive.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_mcp.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_plan.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_restore.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_rules.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_skills.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_status.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_validate.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_cli_workspace_resolution.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_common_repository.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_compiled_planning.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_dto_to_common_mcp.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_mcp_service.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_symlink_planning.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_sync_plan_model.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/tests/test_utils.py +0 -0
- {code_agnostic-0.3.2 → code_agnostic-0.3.4}/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.
|
|
3
|
+
Version: 0.3.4
|
|
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
|
|
@@ -102,7 +102,7 @@ code-agnostic apply
|
|
|
102
102
|
| Agents sync | yes | yes | yes |
|
|
103
103
|
| Workspace root `AGENTS.md` link | yes | yes | yes |
|
|
104
104
|
| Native repo config include for workspace `AGENTS.md` | yes | -- | -- |
|
|
105
|
-
| Repo/subdir gets shared workspace `AGENTS.md` today | yes | -- |
|
|
105
|
+
| Repo/subdir gets shared workspace `AGENTS.md` today | yes | -- | yes |
|
|
106
106
|
| Root-level `AGENTS.md` discovery only | -- | yes | yes |
|
|
107
107
|
| Workspace propagation | yes | -- | yes |
|
|
108
108
|
| Import from | yes | yes | yes |
|
|
@@ -110,9 +110,9 @@ code-agnostic apply
|
|
|
110
110
|
|
|
111
111
|
Cursor workspace propagation is intentionally disabled to avoid duplicate MCP initialization in multi-root workspaces: https://forum.cursor.com/t/mcp-multi-root-workspace-causes-duplicate-mcp-server-initialization-4x-createclient-actions/144003
|
|
112
112
|
|
|
113
|
-
OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions.
|
|
113
|
+
OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions. Codex repos receive workspace instructions through a generated `AGENTS.override.md`, which is added to each repo's `.git/info/exclude`.
|
|
114
114
|
|
|
115
|
-
Cursor documents `AGENTS.md` as a root-level project file. Codex documents `AGENTS.md` discovery, but not a native config include for an extra workspace file. In practice that means subdirectories and repos opened below the workspace root will not reliably get the shared workspace `AGENTS.md` today for Cursor
|
|
115
|
+
Cursor documents `AGENTS.md` as a root-level project file. Codex documents `AGENTS.md` discovery, but not a native config include for an extra workspace file. In practice that means subdirectories and repos opened below the workspace root will not reliably get the shared workspace `AGENTS.md` today for Cursor.
|
|
116
116
|
|
|
117
117
|
## Features
|
|
118
118
|
|
|
@@ -171,7 +171,7 @@ code-agnostic agents list
|
|
|
171
171
|
|
|
172
172
|
### Workspaces
|
|
173
173
|
|
|
174
|
-
Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md`
|
|
174
|
+
Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md` at the workspace root. Repos keep their own repo-specific `AGENTS.md`; Codex receives the workspace rules through generated, git-excluded `AGENTS.override.md` files, while OpenCode workspace configs reference the shared workspace file through `instructions`. Repo-local app config, skills, and agents are propagated for OpenCode and Codex.
|
|
175
175
|
|
|
176
176
|
`.cursor` workspace propagation is intentionally disabled to avoid duplicate MCP initialization when opening multi-root workspaces in Cursor (related issue: https://forum.cursor.com/t/mcp-multi-root-workspace-causes-duplicate-mcp-server-initialization-4x-createclient-actions/144003).
|
|
177
177
|
|
|
@@ -79,7 +79,7 @@ code-agnostic apply
|
|
|
79
79
|
| Agents sync | yes | yes | yes |
|
|
80
80
|
| Workspace root `AGENTS.md` link | yes | yes | yes |
|
|
81
81
|
| Native repo config include for workspace `AGENTS.md` | yes | -- | -- |
|
|
82
|
-
| Repo/subdir gets shared workspace `AGENTS.md` today | yes | -- |
|
|
82
|
+
| Repo/subdir gets shared workspace `AGENTS.md` today | yes | -- | yes |
|
|
83
83
|
| Root-level `AGENTS.md` discovery only | -- | yes | yes |
|
|
84
84
|
| Workspace propagation | yes | -- | yes |
|
|
85
85
|
| Import from | yes | yes | yes |
|
|
@@ -87,9 +87,9 @@ code-agnostic apply
|
|
|
87
87
|
|
|
88
88
|
Cursor workspace propagation is intentionally disabled to avoid duplicate MCP initialization in multi-root workspaces: https://forum.cursor.com/t/mcp-multi-root-workspace-causes-duplicate-mcp-server-initialization-4x-createclient-actions/144003
|
|
89
89
|
|
|
90
|
-
OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions.
|
|
90
|
+
OpenCode workspace configs include the shared workspace `AGENTS.md` natively via `instructions`, so repos under the workspace get both repo-local and shared workspace instructions. Codex repos receive workspace instructions through a generated `AGENTS.override.md`, which is added to each repo's `.git/info/exclude`.
|
|
91
91
|
|
|
92
|
-
Cursor documents `AGENTS.md` as a root-level project file. Codex documents `AGENTS.md` discovery, but not a native config include for an extra workspace file. In practice that means subdirectories and repos opened below the workspace root will not reliably get the shared workspace `AGENTS.md` today for Cursor
|
|
92
|
+
Cursor documents `AGENTS.md` as a root-level project file. Codex documents `AGENTS.md` discovery, but not a native config include for an extra workspace file. In practice that means subdirectories and repos opened below the workspace root will not reliably get the shared workspace `AGENTS.md` today for Cursor.
|
|
93
93
|
|
|
94
94
|
## Features
|
|
95
95
|
|
|
@@ -148,7 +148,7 @@ code-agnostic agents list
|
|
|
148
148
|
|
|
149
149
|
### Workspaces
|
|
150
150
|
|
|
151
|
-
Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md`
|
|
151
|
+
Register workspace directories. Workspace rules are compiled into a canonical `AGENTS.md` at the workspace root. Repos keep their own repo-specific `AGENTS.md`; Codex receives the workspace rules through generated, git-excluded `AGENTS.override.md` files, while OpenCode workspace configs reference the shared workspace file through `instructions`. Repo-local app config, skills, and agents are propagated for OpenCode and Codex.
|
|
152
152
|
|
|
153
153
|
`.cursor` workspace propagation is intentionally disabled to avoid duplicate MCP initialization when opening multi-root workspaces in Cursor (related issue: https://forum.cursor.com/t/mcp-multi-root-workspace-causes-duplicate-mcp-server-initialization-4x-createclient-actions/144003).
|
|
154
154
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""CLI entrypoint - thin wrapper that wires command modules."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from code_agnostic.cli import AliasedGroup
|
|
6
|
+
from code_agnostic.cli.commands.agents import agents_group
|
|
7
|
+
from code_agnostic.cli.commands.apps import apps
|
|
8
|
+
from code_agnostic.cli.commands.apply import apply
|
|
9
|
+
from code_agnostic.cli.commands.explain_lossiness import explain_lossiness
|
|
10
|
+
from code_agnostic.cli.commands.import_ import import_group
|
|
11
|
+
from code_agnostic.cli.commands.mcp import mcp
|
|
12
|
+
from code_agnostic.cli.commands.plan import plan
|
|
13
|
+
from code_agnostic.cli.commands.restore import restore
|
|
14
|
+
from code_agnostic.cli.commands.rules import rules
|
|
15
|
+
from code_agnostic.cli.commands.skills import skills
|
|
16
|
+
from code_agnostic.cli.commands.status import status
|
|
17
|
+
from code_agnostic.cli.commands.validate import validate
|
|
18
|
+
from code_agnostic.cli.commands.workspaces import workspaces
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group(
|
|
22
|
+
cls=AliasedGroup,
|
|
23
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
24
|
+
)
|
|
25
|
+
@click.pass_context
|
|
26
|
+
def cli(ctx: click.Context) -> None:
|
|
27
|
+
"""App-based config sync."""
|
|
28
|
+
ctx.obj = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Register individual commands
|
|
32
|
+
cli.add_command(plan)
|
|
33
|
+
cli.add_command(apply)
|
|
34
|
+
cli.add_command(restore)
|
|
35
|
+
cli.add_command(status)
|
|
36
|
+
cli.add_command(validate)
|
|
37
|
+
cli.add_command(explain_lossiness)
|
|
38
|
+
|
|
39
|
+
# Register command groups
|
|
40
|
+
cli.add_command(apps)
|
|
41
|
+
cli.add_command(workspaces)
|
|
42
|
+
cli.add_command(rules)
|
|
43
|
+
cli.add_command(skills)
|
|
44
|
+
cli.add_command(agents_group)
|
|
45
|
+
cli.add_command(mcp)
|
|
46
|
+
cli.add_command(import_group)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main() -> int:
|
|
50
|
+
try:
|
|
51
|
+
cli(standalone_mode=False)
|
|
52
|
+
except click.exceptions.Exit as exc:
|
|
53
|
+
code = exc.exit_code
|
|
54
|
+
return code if isinstance(code, int) else 1
|
|
55
|
+
except click.ClickException as exc:
|
|
56
|
+
exc.show()
|
|
57
|
+
return 2
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
raise SystemExit(main())
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
|
|
4
|
+
from code_agnostic.constants import (
|
|
5
|
+
CODEX_CONFIG_FILENAME,
|
|
6
|
+
CODEX_PROJECT_DIRNAME,
|
|
7
|
+
CURSOR_CONFIG_FILENAME,
|
|
8
|
+
CURSOR_PROJECT_DIRNAME,
|
|
9
|
+
OPENCODE_CONFIG_FILENAME,
|
|
10
|
+
OPENCODE_PROJECT_DIRNAME,
|
|
11
|
+
)
|
|
12
|
+
|
|
4
13
|
|
|
5
14
|
class AppId(str, Enum):
|
|
6
15
|
CORE = "core"
|
|
@@ -19,6 +28,7 @@ class AppMetadata:
|
|
|
19
28
|
supports_import_agents: bool
|
|
20
29
|
supports_workspace_propagation: bool
|
|
21
30
|
project_dir_name: str | None = None
|
|
31
|
+
config_filename: str | None = None
|
|
22
32
|
|
|
23
33
|
|
|
24
34
|
APP_CATALOG: dict[AppId, AppMetadata] = {
|
|
@@ -40,7 +50,8 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
|
|
|
40
50
|
importable=True,
|
|
41
51
|
supports_import_agents=True,
|
|
42
52
|
supports_workspace_propagation=True,
|
|
43
|
-
project_dir_name=
|
|
53
|
+
project_dir_name=OPENCODE_PROJECT_DIRNAME,
|
|
54
|
+
config_filename=OPENCODE_CONFIG_FILENAME,
|
|
44
55
|
),
|
|
45
56
|
AppId.CURSOR: AppMetadata(
|
|
46
57
|
app_id=AppId.CURSOR,
|
|
@@ -49,8 +60,9 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
|
|
|
49
60
|
toggleable=True,
|
|
50
61
|
importable=True,
|
|
51
62
|
supports_import_agents=True,
|
|
52
|
-
supports_workspace_propagation=
|
|
53
|
-
project_dir_name=
|
|
63
|
+
supports_workspace_propagation=True,
|
|
64
|
+
project_dir_name=CURSOR_PROJECT_DIRNAME,
|
|
65
|
+
config_filename=CURSOR_CONFIG_FILENAME,
|
|
54
66
|
),
|
|
55
67
|
AppId.CODEX: AppMetadata(
|
|
56
68
|
app_id=AppId.CODEX,
|
|
@@ -60,7 +72,8 @@ APP_CATALOG: dict[AppId, AppMetadata] = {
|
|
|
60
72
|
importable=True,
|
|
61
73
|
supports_import_agents=True,
|
|
62
74
|
supports_workspace_propagation=True,
|
|
63
|
-
project_dir_name=
|
|
75
|
+
project_dir_name=CODEX_PROJECT_DIRNAME,
|
|
76
|
+
config_filename=CODEX_CONFIG_FILENAME,
|
|
64
77
|
),
|
|
65
78
|
}
|
|
66
79
|
|
|
@@ -74,6 +87,11 @@ def app_label(app: AppId | str) -> str:
|
|
|
74
87
|
return app_metadata(app).label
|
|
75
88
|
|
|
76
89
|
|
|
90
|
+
def app_scope(app: AppId | str, resource: str) -> str:
|
|
91
|
+
app_id = app if isinstance(app, AppId) else AppId(app)
|
|
92
|
+
return f"app:{app_id.value}:{resource}"
|
|
93
|
+
|
|
94
|
+
|
|
77
95
|
def app_ids_by_capability(
|
|
78
96
|
*,
|
|
79
97
|
targetable: bool | None = None,
|
|
@@ -9,12 +9,18 @@ except ModuleNotFoundError: # pragma: no cover
|
|
|
9
9
|
import tomlkit
|
|
10
10
|
|
|
11
11
|
from code_agnostic.apps.common.interfaces.repositories import IAppConfigRepository
|
|
12
|
+
from code_agnostic.constants import (
|
|
13
|
+
AGENTS_DIRNAME,
|
|
14
|
+
CODEX_CONFIG_FILENAME,
|
|
15
|
+
CODEX_PROJECT_DIRNAME,
|
|
16
|
+
SKILLS_DIRNAME,
|
|
17
|
+
)
|
|
12
18
|
from code_agnostic.errors import InvalidConfigSchemaError, InvalidJsonFormatError
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
class CodexConfigRepository(IAppConfigRepository):
|
|
16
22
|
def __init__(self, root: Path | None = None) -> None:
|
|
17
|
-
self._root = root or (Path.home() /
|
|
23
|
+
self._root = root or (Path.home() / CODEX_PROJECT_DIRNAME)
|
|
18
24
|
|
|
19
25
|
@property
|
|
20
26
|
def root(self) -> Path:
|
|
@@ -22,15 +28,15 @@ class CodexConfigRepository(IAppConfigRepository):
|
|
|
22
28
|
|
|
23
29
|
@property
|
|
24
30
|
def config_path(self) -> Path:
|
|
25
|
-
return self.root /
|
|
31
|
+
return self.root / CODEX_CONFIG_FILENAME
|
|
26
32
|
|
|
27
33
|
@property
|
|
28
34
|
def skills_dir(self) -> Path:
|
|
29
|
-
return self.root /
|
|
35
|
+
return self.root / SKILLS_DIRNAME
|
|
30
36
|
|
|
31
37
|
@property
|
|
32
38
|
def agents_dir(self) -> Path:
|
|
33
|
-
return self.root /
|
|
39
|
+
return self.root / AGENTS_DIRNAME
|
|
34
40
|
|
|
35
41
|
def load_config(self) -> dict[str, Any]:
|
|
36
42
|
if not self.config_path.exists() or self.config_path.stat().st_size == 0:
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"AbsolutePathBuf": {
|
|
19
19
|
"type": "string"
|
|
20
20
|
},
|
|
21
|
+
"AppToolApproval": {
|
|
22
|
+
"enum": ["auto", "prompt", "approve"],
|
|
23
|
+
"type": "string"
|
|
24
|
+
},
|
|
21
25
|
"AgentRoleToml": {
|
|
22
26
|
"type": "object",
|
|
23
27
|
"additionalProperties": false,
|
|
@@ -41,28 +45,68 @@
|
|
|
41
45
|
"$ref": "#/definitions/AgentRoleToml"
|
|
42
46
|
}
|
|
43
47
|
},
|
|
48
|
+
"McpServerToolConfig": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"additionalProperties": false,
|
|
51
|
+
"properties": {
|
|
52
|
+
"approval_mode": {
|
|
53
|
+
"allOf": [{ "$ref": "#/definitions/AppToolApproval" }],
|
|
54
|
+
"description": "Approval mode for this tool."
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"description": "Per-tool approval settings for a single MCP server tool."
|
|
58
|
+
},
|
|
59
|
+
"McpServerOAuthConfig": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"additionalProperties": false,
|
|
62
|
+
"properties": {
|
|
63
|
+
"client_id": { "type": "string" }
|
|
64
|
+
},
|
|
65
|
+
"description": "OAuth client settings used when Codex launches an MCP OAuth flow."
|
|
66
|
+
},
|
|
44
67
|
"RawMcpServerConfig": {
|
|
45
68
|
"type": "object",
|
|
46
69
|
"additionalProperties": false,
|
|
47
70
|
"properties": {
|
|
48
71
|
"args": { "type": "array", "items": { "type": "string" } },
|
|
49
|
-
"bearer_token": { "type": "string" },
|
|
50
72
|
"bearer_token_env_var": { "type": "string" },
|
|
51
73
|
"command": { "type": "string" },
|
|
52
74
|
"cwd": { "type": "string" },
|
|
75
|
+
"default_tools_approval_mode": {
|
|
76
|
+
"allOf": [{ "$ref": "#/definitions/AppToolApproval" }],
|
|
77
|
+
"default": null
|
|
78
|
+
},
|
|
53
79
|
"disabled_tools": { "type": "array", "items": { "type": "string" } },
|
|
54
80
|
"enabled": { "type": "boolean" },
|
|
55
81
|
"enabled_tools": { "type": "array", "items": { "type": "string" } },
|
|
56
82
|
"env": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
57
83
|
"env_http_headers": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
58
84
|
"env_vars": { "type": "array", "items": { "type": "string" } },
|
|
85
|
+
"experimental_environment": { "type": "string" },
|
|
59
86
|
"http_headers": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
87
|
+
"name": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"default": null,
|
|
90
|
+
"description": "Legacy display-name field accepted for backward compatibility."
|
|
91
|
+
},
|
|
92
|
+
"oauth": {
|
|
93
|
+
"allOf": [{ "$ref": "#/definitions/McpServerOAuthConfig" }],
|
|
94
|
+
"default": null
|
|
95
|
+
},
|
|
60
96
|
"oauth_resource": { "type": "string", "default": null },
|
|
61
97
|
"required": { "type": "boolean" },
|
|
62
98
|
"scopes": { "type": "array", "items": { "type": "string" } },
|
|
63
99
|
"startup_timeout_ms": { "type": "integer", "minimum": 0 },
|
|
64
100
|
"startup_timeout_sec": { "type": "number" },
|
|
101
|
+
"supports_parallel_tool_calls": { "type": "boolean" },
|
|
65
102
|
"tool_timeout_sec": { "type": "number" },
|
|
103
|
+
"tools": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"default": null,
|
|
106
|
+
"additionalProperties": {
|
|
107
|
+
"$ref": "#/definitions/McpServerToolConfig"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
66
110
|
"url": { "type": "string" }
|
|
67
111
|
}
|
|
68
112
|
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from jsonschema import Draft7Validator
|
|
6
|
+
|
|
7
|
+
from code_agnostic.agents.codex import normalize_codex_agent_filename
|
|
8
|
+
from code_agnostic.agents.compilers import CodexAgentCompiler
|
|
9
|
+
from code_agnostic.agents.parser import parse_agent
|
|
10
|
+
from code_agnostic.apps.app_id import AppId, app_label
|
|
11
|
+
from code_agnostic.apps.common.framework import (
|
|
12
|
+
RegisteredAppConfigService,
|
|
13
|
+
format_schema_error,
|
|
14
|
+
)
|
|
15
|
+
from code_agnostic.apps.codex.config_repository import CodexConfigRepository
|
|
16
|
+
from code_agnostic.apps.codex.mapper import CodexMCPMapper
|
|
17
|
+
from code_agnostic.apps.codex.schema_repository import CodexSchemaRepository
|
|
18
|
+
from code_agnostic.apps.common.interfaces.mapper import IAppMCPMapper
|
|
19
|
+
from code_agnostic.apps.common.interfaces.repositories import (
|
|
20
|
+
IAppConfigRepository,
|
|
21
|
+
ISchemaRepository,
|
|
22
|
+
)
|
|
23
|
+
from code_agnostic.apps.common.models import MCPServerDTO
|
|
24
|
+
from code_agnostic.errors import (
|
|
25
|
+
InvalidConfigSchemaError,
|
|
26
|
+
InvalidJsonFormatError,
|
|
27
|
+
)
|
|
28
|
+
from code_agnostic.models import Action, ActionKind, ActionStatus
|
|
29
|
+
from code_agnostic.utils import merge_dict_overlay, read_json_safe
|
|
30
|
+
from code_agnostic.skills.compilers import CodexSkillCompiler
|
|
31
|
+
from code_agnostic.skills.parser import parse_skill
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CodexConfigService(RegisteredAppConfigService):
|
|
35
|
+
APP_ID = AppId.CODEX
|
|
36
|
+
APP_LABEL = app_label(APP_ID)
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
repository: CodexConfigRepository,
|
|
41
|
+
mapper: IAppMCPMapper,
|
|
42
|
+
schema_repository: ISchemaRepository,
|
|
43
|
+
base_config_path: Path | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
self._repository = repository
|
|
46
|
+
self._codex_repo = repository
|
|
47
|
+
self._mapper = mapper
|
|
48
|
+
self._schema_repository = schema_repository
|
|
49
|
+
self._base_config_path = base_config_path
|
|
50
|
+
self._validator = Draft7Validator(self._schema_repository.load_schema())
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def create_default(cls, root: Path | None = None) -> "CodexConfigService":
|
|
54
|
+
if root is not None:
|
|
55
|
+
return cls(
|
|
56
|
+
repository=CodexConfigRepository(root=root),
|
|
57
|
+
mapper=CodexMCPMapper(),
|
|
58
|
+
schema_repository=CodexSchemaRepository(),
|
|
59
|
+
base_config_path=None,
|
|
60
|
+
)
|
|
61
|
+
from code_agnostic.core.repository import CoreRepository
|
|
62
|
+
|
|
63
|
+
core = CoreRepository()
|
|
64
|
+
return cls(
|
|
65
|
+
repository=CodexConfigRepository(root=root),
|
|
66
|
+
mapper=CodexMCPMapper(),
|
|
67
|
+
schema_repository=CodexSchemaRepository(),
|
|
68
|
+
base_config_path=core.codex_base_path,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def app_id(self) -> AppId:
|
|
73
|
+
return self.APP_ID
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def action_kind(self) -> ActionKind:
|
|
77
|
+
return ActionKind.WRITE_TEXT
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def repository(self) -> IAppConfigRepository:
|
|
81
|
+
return self._repository
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def mapper(self) -> IAppMCPMapper:
|
|
85
|
+
return self._mapper
|
|
86
|
+
|
|
87
|
+
def validate_config(self, payload: Any) -> None:
|
|
88
|
+
error = next(iter(self._validator.iter_errors(payload)), None)
|
|
89
|
+
if error is not None:
|
|
90
|
+
raise InvalidConfigSchemaError(
|
|
91
|
+
self.repository.config_path, format_schema_error(error)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def build_action_payload(self, payload: dict[str, Any]) -> Any:
|
|
95
|
+
return self.repository.serialize_config(payload)
|
|
96
|
+
|
|
97
|
+
def set_mcp_payload(
|
|
98
|
+
self, merged: dict[str, Any], desired_mcp: dict[str, Any]
|
|
99
|
+
) -> None:
|
|
100
|
+
merged["mcp_servers"] = desired_mcp
|
|
101
|
+
|
|
102
|
+
def derive_status(
|
|
103
|
+
self, existing: dict[str, Any], merged: dict[str, Any]
|
|
104
|
+
) -> ActionStatus:
|
|
105
|
+
rendered = self.repository.serialize_config(merged)
|
|
106
|
+
existing_text = (
|
|
107
|
+
self.repository.config_path.read_text(encoding="utf-8")
|
|
108
|
+
if self.repository.config_path.exists()
|
|
109
|
+
else ""
|
|
110
|
+
)
|
|
111
|
+
if not self.repository.config_path.exists():
|
|
112
|
+
return ActionStatus.CREATE
|
|
113
|
+
if existing_text == rendered:
|
|
114
|
+
return ActionStatus.NOOP
|
|
115
|
+
return ActionStatus.UPDATE
|
|
116
|
+
|
|
117
|
+
def build_action(
|
|
118
|
+
self,
|
|
119
|
+
common_servers: dict[str, MCPServerDTO],
|
|
120
|
+
agent_sources: list[Path] | None = None,
|
|
121
|
+
) -> Action:
|
|
122
|
+
existing = self._codex_repo.load_config()
|
|
123
|
+
if existing or self._codex_repo.config_path.exists():
|
|
124
|
+
self.validate_config(existing)
|
|
125
|
+
|
|
126
|
+
desired_mcp = self.mapper.from_common(common_servers)
|
|
127
|
+
merged = dict(existing)
|
|
128
|
+
base = self._load_base_config()
|
|
129
|
+
for key, value in base.items():
|
|
130
|
+
if key == "mcp_servers":
|
|
131
|
+
continue
|
|
132
|
+
if key == "agents" and isinstance(value, dict):
|
|
133
|
+
merged["agents"] = self._merge_agents_payload(
|
|
134
|
+
merged.get("agents"), value
|
|
135
|
+
)
|
|
136
|
+
continue
|
|
137
|
+
current = merged.get(key)
|
|
138
|
+
if isinstance(current, dict) and isinstance(value, dict):
|
|
139
|
+
merged[key] = merge_dict_overlay(current, value)
|
|
140
|
+
continue
|
|
141
|
+
merged[key] = deepcopy(value)
|
|
142
|
+
self.set_mcp_payload(merged, desired_mcp)
|
|
143
|
+
if agent_sources:
|
|
144
|
+
merged["agents"] = self._merge_agents_payload(
|
|
145
|
+
merged.get("agents"),
|
|
146
|
+
self._build_agent_registry(agent_sources),
|
|
147
|
+
)
|
|
148
|
+
self.validate_config(merged)
|
|
149
|
+
|
|
150
|
+
return Action(
|
|
151
|
+
kind=self.action_kind,
|
|
152
|
+
path=self.repository.config_path,
|
|
153
|
+
status=self.derive_status(existing, merged),
|
|
154
|
+
detail=f"sync {self.app_id.value} config from common mcp base",
|
|
155
|
+
payload=self.build_action_payload(merged),
|
|
156
|
+
app=self.app_id.value,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _merge_agents_payload(
|
|
160
|
+
self, existing: Any, overlay: dict[str, Any]
|
|
161
|
+
) -> dict[str, Any]:
|
|
162
|
+
merged = dict(existing) if isinstance(existing, dict) else {}
|
|
163
|
+
for key, value in overlay.items():
|
|
164
|
+
merged[key] = deepcopy(value)
|
|
165
|
+
return merged
|
|
166
|
+
|
|
167
|
+
def _build_agent_registry(self, sources: list[Path]) -> dict[str, dict[str, Any]]:
|
|
168
|
+
registry: dict[str, dict[str, Any]] = {}
|
|
169
|
+
for source in sources:
|
|
170
|
+
try:
|
|
171
|
+
agent = parse_agent(source)
|
|
172
|
+
except InvalidConfigSchemaError:
|
|
173
|
+
raise
|
|
174
|
+
except Exception as exc:
|
|
175
|
+
raise InvalidConfigSchemaError(source, str(exc)) from exc
|
|
176
|
+
|
|
177
|
+
agent_name = agent.metadata.name or agent.name
|
|
178
|
+
target_name = (
|
|
179
|
+
normalize_codex_agent_filename(agent.metadata.name, agent.name)
|
|
180
|
+
+ ".toml"
|
|
181
|
+
)
|
|
182
|
+
entry: dict[str, Any] = {
|
|
183
|
+
"description": agent.metadata.description or agent_name,
|
|
184
|
+
"config_file": (Path("agents") / target_name).as_posix(),
|
|
185
|
+
}
|
|
186
|
+
if agent.metadata.nickname_candidates:
|
|
187
|
+
entry["nickname_candidates"] = list(agent.metadata.nickname_candidates)
|
|
188
|
+
registry[agent_name] = entry
|
|
189
|
+
return registry
|
|
190
|
+
|
|
191
|
+
def plan_skill_actions(
|
|
192
|
+
self,
|
|
193
|
+
sources: list[Path],
|
|
194
|
+
target_dir: Path,
|
|
195
|
+
scope: str,
|
|
196
|
+
app: str,
|
|
197
|
+
managed_paths: list[Path],
|
|
198
|
+
removable_links: list[Path],
|
|
199
|
+
) -> tuple[list[Action], list[Path], list[str]]:
|
|
200
|
+
compiler = CodexSkillCompiler()
|
|
201
|
+
return self._plan_compiled_text_actions(
|
|
202
|
+
sources=sources,
|
|
203
|
+
target_dir=target_dir,
|
|
204
|
+
scope=scope,
|
|
205
|
+
app=app,
|
|
206
|
+
managed_paths=managed_paths,
|
|
207
|
+
removable_links=removable_links,
|
|
208
|
+
compile_source=lambda source: (
|
|
209
|
+
target_dir / source.name / "SKILL.md",
|
|
210
|
+
compiler.compile(
|
|
211
|
+
parse_skill(
|
|
212
|
+
source / "SKILL.md"
|
|
213
|
+
if (source / "SKILL.md").exists()
|
|
214
|
+
else source
|
|
215
|
+
)
|
|
216
|
+
),
|
|
217
|
+
),
|
|
218
|
+
create_detail="create compiled codex skill",
|
|
219
|
+
noop_detail="compiled codex skill already up to date",
|
|
220
|
+
update_detail="update compiled codex skill",
|
|
221
|
+
conflict_message="Codex skill sync skipped (conflict): {target}",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def plan_agent_actions(
|
|
225
|
+
self,
|
|
226
|
+
sources: list[Path],
|
|
227
|
+
target_dir: Path,
|
|
228
|
+
scope: str,
|
|
229
|
+
app: str,
|
|
230
|
+
managed_paths: list[Path],
|
|
231
|
+
removable_links: list[Path],
|
|
232
|
+
) -> tuple[list[Action], list[Path], list[str]]:
|
|
233
|
+
compiler = CodexAgentCompiler()
|
|
234
|
+
|
|
235
|
+
def compile_source(source: Path) -> tuple[Path, str]:
|
|
236
|
+
try:
|
|
237
|
+
agent = parse_agent(source)
|
|
238
|
+
payload = compiler.compile(agent)
|
|
239
|
+
except InvalidConfigSchemaError:
|
|
240
|
+
raise
|
|
241
|
+
except Exception as exc:
|
|
242
|
+
raise InvalidConfigSchemaError(source, str(exc)) from exc
|
|
243
|
+
|
|
244
|
+
target_name = (
|
|
245
|
+
normalize_codex_agent_filename(agent.metadata.name, agent.name)
|
|
246
|
+
+ ".toml"
|
|
247
|
+
)
|
|
248
|
+
return target_dir / target_name, payload
|
|
249
|
+
|
|
250
|
+
return self._plan_compiled_text_actions(
|
|
251
|
+
sources=sources,
|
|
252
|
+
target_dir=target_dir,
|
|
253
|
+
scope=scope,
|
|
254
|
+
app=app,
|
|
255
|
+
managed_paths=managed_paths,
|
|
256
|
+
removable_links=removable_links,
|
|
257
|
+
compile_source=compile_source,
|
|
258
|
+
create_detail="create compiled codex agent",
|
|
259
|
+
noop_detail="compiled codex agent already up to date",
|
|
260
|
+
update_detail="update compiled codex agent",
|
|
261
|
+
conflict_message="Codex agent sync skipped (conflict): {target}",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def _load_base_config(self) -> dict[str, Any]:
|
|
265
|
+
if self._base_config_path is None or not self._base_config_path.exists():
|
|
266
|
+
return {}
|
|
267
|
+
payload, error = read_json_safe(self._base_config_path)
|
|
268
|
+
if error is not None:
|
|
269
|
+
raise InvalidJsonFormatError(self._base_config_path, error)
|
|
270
|
+
if not isinstance(payload, dict):
|
|
271
|
+
raise InvalidConfigSchemaError(
|
|
272
|
+
self._base_config_path, "must be a JSON object"
|
|
273
|
+
)
|
|
274
|
+
return payload
|
|
@@ -3,6 +3,18 @@ from pathlib import Path
|
|
|
3
3
|
from code_agnostic.models import Action, ActionKind, ActionStatus
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
def find_replaceable_symlink_ancestor(target: Path, managed_root: Path) -> Path | None:
|
|
7
|
+
current = target
|
|
8
|
+
while True:
|
|
9
|
+
if current.is_symlink() and (
|
|
10
|
+
current == managed_root or current.is_relative_to(managed_root)
|
|
11
|
+
):
|
|
12
|
+
return current
|
|
13
|
+
if current.parent == current:
|
|
14
|
+
return None
|
|
15
|
+
current = current.parent
|
|
16
|
+
|
|
17
|
+
|
|
6
18
|
def _symlink_ancestor_state(
|
|
7
19
|
target: Path, removable_link_paths: set[Path]
|
|
8
20
|
) -> tuple[bool, bool]:
|