graph-agents-cli 0.3.1__py3-none-any.whl
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.
- graph_agents_cli/__init__.py +26 -0
- graph_agents_cli/_api_policy.py +2145 -0
- graph_agents_cli/_approvals.py +400 -0
- graph_agents_cli/_build.py +186 -0
- graph_agents_cli/_build_info.json +7 -0
- graph_agents_cli/_chat_client.py +462 -0
- graph_agents_cli/_click.py +157 -0
- graph_agents_cli/_defaults.py +139 -0
- graph_agents_cli/_experiments.py +64 -0
- graph_agents_cli/_http.py +192 -0
- graph_agents_cli/_output.py +83 -0
- graph_agents_cli/_project.py +462 -0
- graph_agents_cli/_remote.py +220 -0
- graph_agents_cli/_response_schema.py +264 -0
- graph_agents_cli/_runner.py +319 -0
- graph_agents_cli/_skills_check.py +274 -0
- graph_agents_cli/_tools.py +189 -0
- graph_agents_cli/_trust.py +66 -0
- graph_agents_cli/api/__init__.py +15 -0
- graph_agents_cli/api/_changes.py +506 -0
- graph_agents_cli/api/_files.py +658 -0
- graph_agents_cli/api/cmd_api.py +2480 -0
- graph_agents_cli/deploy/__init__.py +15 -0
- graph_agents_cli/deploy/_config.py +171 -0
- graph_agents_cli/deploy/_image.py +128 -0
- graph_agents_cli/deploy/_kube.py +286 -0
- graph_agents_cli/deploy/_modes.py +234 -0
- graph_agents_cli/deploy/_preflight.py +370 -0
- graph_agents_cli/deploy/_values.py +168 -0
- graph_agents_cli/deploy/cmd_deploy.py +1866 -0
- graph_agents_cli/deploy/gitops.py +562 -0
- graph_agents_cli/deploy/local_load.py +273 -0
- graph_agents_cli/dev/__init__.py +13 -0
- graph_agents_cli/dev/cmd_build.py +131 -0
- graph_agents_cli/dev/cmd_install.py +78 -0
- graph_agents_cli/dev/cmd_lint.py +119 -0
- graph_agents_cli/dev/cmd_playground.py +297 -0
- graph_agents_cli/dev/policy_check.py +1287 -0
- graph_agents_cli/eval/__init__.py +22 -0
- graph_agents_cli/eval/_client.py +670 -0
- graph_agents_cli/eval/_common.py +177 -0
- graph_agents_cli/eval/_judge.py +168 -0
- graph_agents_cli/eval/_judge_runner.py +238 -0
- graph_agents_cli/eval/_paths.py +212 -0
- graph_agents_cli/eval/checks.py +581 -0
- graph_agents_cli/eval/cmd_analyze.py +278 -0
- graph_agents_cli/eval/cmd_compare.py +284 -0
- graph_agents_cli/eval/cmd_eval_group.py +80 -0
- graph_agents_cli/eval/cmd_generate.py +558 -0
- graph_agents_cli/eval/cmd_grade.py +466 -0
- graph_agents_cli/eval/cmd_metric.py +156 -0
- graph_agents_cli/eval/cmd_run.py +370 -0
- graph_agents_cli/eval/cmd_submit.py +400 -0
- graph_agents_cli/eval/config.py +435 -0
- graph_agents_cli/eval/dataset.py +350 -0
- graph_agents_cli/eval/gate.py +420 -0
- graph_agents_cli/eval/transcript.py +192 -0
- graph_agents_cli/extension/__init__.py +13 -0
- graph_agents_cli/extension/_compat.py +86 -0
- graph_agents_cli/extension/_loader.py +293 -0
- graph_agents_cli/extension/_manifest.py +135 -0
- graph_agents_cli/extension/_overrides.py +195 -0
- graph_agents_cli/extension/_paths.py +91 -0
- graph_agents_cli/extension/_refs.py +193 -0
- graph_agents_cli/extension/_resolver.py +453 -0
- graph_agents_cli/extension/_schema.py +106 -0
- graph_agents_cli/extension/_spec.py +253 -0
- graph_agents_cli/extension/_sync.py +102 -0
- graph_agents_cli/extension/_trust.py +58 -0
- graph_agents_cli/extension/cmd_extension_add.py +259 -0
- graph_agents_cli/extension/cmd_extension_group.py +57 -0
- graph_agents_cli/extension/cmd_extension_list.py +56 -0
- graph_agents_cli/extension/cmd_extension_remove.py +61 -0
- graph_agents_cli/extension/cmd_extension_update.py +195 -0
- graph_agents_cli/info/__init__.py +13 -0
- graph_agents_cli/info/cmd_info.py +222 -0
- graph_agents_cli/infra/__init__.py +15 -0
- graph_agents_cli/infra/checks.py +1169 -0
- graph_agents_cli/infra/cmd_infra.py +103 -0
- graph_agents_cli/main.py +591 -0
- graph_agents_cli/peer/__init__.py +15 -0
- graph_agents_cli/peer/_generate.py +254 -0
- graph_agents_cli/peer/cmd_peer.py +1151 -0
- graph_agents_cli/run/__init__.py +13 -0
- graph_agents_cli/run/_local_server.py +1157 -0
- graph_agents_cli/run/_signals.py +141 -0
- graph_agents_cli/run/cmd_approvals.py +530 -0
- graph_agents_cli/run/cmd_run.py +1421 -0
- graph_agents_cli/scaffold/__init__.py +19 -0
- graph_agents_cli/scaffold/agents/README.md +24 -0
- graph_agents_cli/scaffold/agents/empty_py/.template/templateconfig.yaml +22 -0
- graph_agents_cli/scaffold/agents/langgraph/.env.example +292 -0
- graph_agents_cli/scaffold/agents/langgraph/.template/templateconfig.yaml +28 -0
- graph_agents_cli/scaffold/agents/langgraph/Dockerfile +59 -0
- graph_agents_cli/scaffold/agents/langgraph/Dockerfile.langgraph-server +59 -0
- graph_agents_cli/scaffold/agents/langgraph/README.md +571 -0
- graph_agents_cli/scaffold/agents/langgraph/api-policy.yaml +60 -0
- graph_agents_cli/scaffold/agents/langgraph/app/__init__.py +20 -0
- graph_agents_cli/scaffold/agents/langgraph/app/agent.py +174 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/__init__.py +15 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a.py +2162 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a_client.py +1167 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/api_client.py +4220 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/approvals.py +1349 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/auth.py +1986 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/chat.py +2962 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/checkpointer.py +432 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/content.py +569 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/db.py +580 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/limits.py +203 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/metrics.py +231 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/middleware.py +361 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/model.py +611 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/playground.py +230 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/run_locks.py +459 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/structured.py +755 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/telemetry.py +681 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/threads.py +493 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/token_exchange.py +959 -0
- graph_agents_cli/scaffold/agents/langgraph/app/fast_api_app.py +770 -0
- graph_agents_cli/scaffold/agents/langgraph/app/policies/__init__.py +55 -0
- graph_agents_cli/scaffold/agents/langgraph/app/policies/custom.py +97 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/__init__.py +46 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/example_api.py +92 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/weather.py +33 -0
- graph_agents_cli/scaffold/agents/langgraph/langgraph.json +14 -0
- graph_agents_cli/scaffold/agents/langgraph/pyproject.toml +78 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/conftest.py +376 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/eval/datasets/basic-dataset.json +53 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/eval/eval_config.yaml +32 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/approval_graph.py +137 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_issuer.py +216 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_openai.py +357 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_outcomes.py +569 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_relay.py +479 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_api_surface.py +812 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals.py +1367 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals_server.py +794 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor.py +497 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor_server.py +247 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_history_repair.py +278 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_model_apis.py +242 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_postgres.py +637 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_resilience_postgres.py +770 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_runtime_guardrails.py +854 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_e2e.py +340 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_runtime.py +989 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_answers.py +584 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_server.py +222 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_token_exchange_issuer.py +650 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/.results/.placeholder +0 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/README.md +22 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/conftest.py +21 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/load_test.py +81 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_client.py +824 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_scoping.py +724 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client.py +1214 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client_hardening.py +716 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_policy_rpc.py +767 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_approval_ledger.py +1536 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_fake_model.py +115 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_jwt_policy.py +991 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_limits.py +310 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging.py +148 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging_hardening.py +271 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_policy.py +378 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_resilience.py +610 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_server_auth.py +702 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_structured.py +673 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_telemetry.py +404 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_thread_listing.py +255 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_threads.py +268 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_token_exchange.py +1320 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_untrusted_content.py +393 -0
- graph_agents_cli/scaffold/agents/langgraph/uv-fastapi.lock +2084 -0
- graph_agents_cli/scaffold/agents/langgraph/uv-langgraph-server.lock +2106 -0
- graph_agents_cli/scaffold/agents/langgraph/{{cookiecutter.agent_guidance_filename}} +129 -0
- graph_agents_cli/scaffold/base_templates/_shared/graph-agents-cli-manifest.yaml +36 -0
- graph_agents_cli/scaffold/base_templates/python/.dockerignore +32 -0
- graph_agents_cli/scaffold/base_templates/python/.github/CODEOWNERS +30 -0
- graph_agents_cli/scaffold/base_templates/python/.github/agent.env +7 -0
- graph_agents_cli/scaffold/base_templates/python/.github/workflows/pr_checks.yaml +214 -0
- graph_agents_cli/scaffold/base_templates/python/.gitignore +209 -0
- graph_agents_cli/scaffold/base_templates/python/tests/unit/test_dummy.py +23 -0
- graph_agents_cli/scaffold/base_templates/python/{{cookiecutter.agent_guidance_filename}} +35 -0
- graph_agents_cli/scaffold/cmd_scaffold_group.py +49 -0
- graph_agents_cli/scaffold/commands/__init__.py +13 -0
- graph_agents_cli/scaffold/commands/create.py +1424 -0
- graph_agents_cli/scaffold/commands/enhance.py +1652 -0
- graph_agents_cli/scaffold/commands/upgrade.py +570 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/agent.env +12 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/promote-to-prod.yaml +371 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/staging.yaml +450 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-dev.yaml +43 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-prod.yaml +41 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-staging.yaml +43 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/.helmignore +14 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/Chart.yaml +21 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/examples/networkpolicy.yaml +103 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/NOTES.txt +48 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/_helpers.tpl +189 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/certificate.yaml +15 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/configmap.yaml +10 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/deployment.yaml +199 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/hpa.yaml +22 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/httproute.yaml +30 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/ingress.yaml +39 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/networkpolicy.yaml +48 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/pdb.yaml +13 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/postgresql-secret.yaml +37 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/service.yaml +15 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/serviceaccount.yaml +13 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/servicemonitor.yaml +42 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-dev.yaml +22 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-prod.yaml +45 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-staging.yaml +29 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values.yaml +396 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/tests/integration/test_chart.py +269 -0
- graph_agents_cli/scaffold/deployment_targets/none/README.md +5 -0
- graph_agents_cli/scaffold/deployment_targets/none/python/README.md +6 -0
- graph_agents_cli/scaffold/utils/__init__.py +13 -0
- graph_agents_cli/scaffold/utils/backup.py +212 -0
- graph_agents_cli/scaffold/utils/build_record.py +257 -0
- graph_agents_cli/scaffold/utils/cli_options.py +184 -0
- graph_agents_cli/scaffold/utils/fs.py +83 -0
- graph_agents_cli/scaffold/utils/generate_locks.py +214 -0
- graph_agents_cli/scaffold/utils/generation_metadata.py +88 -0
- graph_agents_cli/scaffold/utils/keyedit.py +768 -0
- graph_agents_cli/scaffold/utils/keymerge.py +537 -0
- graph_agents_cli/scaffold/utils/language.py +138 -0
- graph_agents_cli/scaffold/utils/lock_utils.py +94 -0
- graph_agents_cli/scaffold/utils/logging.py +77 -0
- graph_agents_cli/scaffold/utils/manifest.py +292 -0
- graph_agents_cli/scaffold/utils/merge.py +970 -0
- graph_agents_cli/scaffold/utils/merge3.py +216 -0
- graph_agents_cli/scaffold/utils/openapi_seed.py +199 -0
- graph_agents_cli/scaffold/utils/remote_template.py +376 -0
- graph_agents_cli/scaffold/utils/template.py +1352 -0
- graph_agents_cli/scaffold/utils/upgrade.py +894 -0
- graph_agents_cli/scaffold/utils/version.py +438 -0
- graph_agents_cli/secrets/__init__.py +15 -0
- graph_agents_cli/secrets/_apply.py +954 -0
- graph_agents_cli/secrets/_required.py +188 -0
- graph_agents_cli/secrets/cmd_secrets.py +211 -0
- graph_agents_cli/setup/__init__.py +13 -0
- graph_agents_cli/setup/_antigravity.py +221 -0
- graph_agents_cli/setup/cmd_auth.py +1030 -0
- graph_agents_cli/setup/cmd_dev_token.py +513 -0
- graph_agents_cli/setup/cmd_setup.py +428 -0
- graph_agents_cli/setup/cmd_update.py +140 -0
- graph_agents_cli/skills/__init__.py +13 -0
- graph_agents_cli/skills/_bundle.py +65 -0
- graph_agents_cli/skills/data/README.md +19 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/SKILL.md +357 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/github-settings.md +113 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/gitops.md +137 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/kubernetes.md +315 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/secrets.md +160 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/SKILL.md +303 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/references/dataset_schema.md +282 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/references/metrics-guide.md +143 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/SKILL.md +659 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langchain-models.md +124 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langgraph.md +235 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/template-contract.md +477 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/SKILL.md +231 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/references/langsmith.md +46 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/references/otel.md +59 -0
- graph_agents_cli/skills/data/graph-agents-cli-scaffold/SKILL.md +414 -0
- graph_agents_cli/skills/data/graph-agents-cli-scaffold/references/flags.md +134 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/SKILL.md +478 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/brainstorming.md +118 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/commands.md +419 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/extension.md +156 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/internals.md +67 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/spec-template.md +56 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/terminology.md +119 -0
- graph_agents_cli/system/__init__.py +15 -0
- graph_agents_cli/system/_apply.py +519 -0
- graph_agents_cli/system/_checks.py +1023 -0
- graph_agents_cli/system/_deploy.py +215 -0
- graph_agents_cli/system/_model.py +363 -0
- graph_agents_cli/system/_system.py +664 -0
- graph_agents_cli/system/_views.py +208 -0
- graph_agents_cli/system/cmd_system.py +423 -0
- graph_agents_cli-0.3.1.dist-info/METADATA +162 -0
- graph_agents_cli-0.3.1.dist-info/RECORD +291 -0
- graph_agents_cli-0.3.1.dist-info/WHEEL +4 -0
- graph_agents_cli-0.3.1.dist-info/entry_points.txt +2 -0
- graph_agents_cli-0.3.1.dist-info/licenses/LICENSE +201 -0
- graph_agents_cli-0.3.1.dist-info/licenses/NOTICE +19 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Evaluate an extension's declared graph-agents-cli compatibility range.
|
|
16
|
+
|
|
17
|
+
An extension may declare ``requires.agents_cli`` (e.g. ``">=1.1,<2"``), which is a
|
|
18
|
+
PEP 440 specifier set evaluated against the running CLI version.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
|
26
|
+
from packaging.version import InvalidVersion, Version
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_range(spec: str) -> SpecifierSet | None:
|
|
30
|
+
"""Parse a declared range, or None if it is unusable.
|
|
31
|
+
|
|
32
|
+
The single place a range string is interpreted, so validation and the
|
|
33
|
+
compatibility check cannot disagree about what is parseable.
|
|
34
|
+
"""
|
|
35
|
+
if not spec.strip():
|
|
36
|
+
# packaging accepts "" as "any version"; an empty range in a manifest is
|
|
37
|
+
# an authoring mistake, so drop it rather than record a no-op.
|
|
38
|
+
return None
|
|
39
|
+
try:
|
|
40
|
+
return SpecifierSet(spec)
|
|
41
|
+
except InvalidSpecifier:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def validate_range(spec: str) -> bool:
|
|
46
|
+
"""True iff ``spec`` is a usable version range."""
|
|
47
|
+
return parse_range(spec) is not None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_compatible(version: str, spec: str | None) -> bool:
|
|
51
|
+
"""Return whether ``version`` satisfies the range ``spec``.
|
|
52
|
+
|
|
53
|
+
Assumes ``spec`` already passed ``validate_range``: ``_spec._parse_requires``
|
|
54
|
+
warns and drops a bad range at parse time, so an unparseable one never
|
|
55
|
+
reaches here (and is treated as no constraint if it somehow does).
|
|
56
|
+
|
|
57
|
+
No constraint means compatible. A source build (``DEV_VERSION``, or any
|
|
58
|
+
version packaging can't read) is always compatible: running from a checkout
|
|
59
|
+
must never be blocked by an extension's range.
|
|
60
|
+
"""
|
|
61
|
+
if not spec:
|
|
62
|
+
return True
|
|
63
|
+
try:
|
|
64
|
+
running = Version(version)
|
|
65
|
+
except InvalidVersion:
|
|
66
|
+
# The *running* CLI reporting an unreadable version is a broken install,
|
|
67
|
+
# not an extension problem, so say so rather than fail it closed.
|
|
68
|
+
logging.warning(
|
|
69
|
+
"Cannot read the running graph-agents-cli version %r; skipping the "
|
|
70
|
+
"compatibility check against %r.",
|
|
71
|
+
version,
|
|
72
|
+
spec,
|
|
73
|
+
)
|
|
74
|
+
return True
|
|
75
|
+
if running.is_devrelease:
|
|
76
|
+
logging.debug(
|
|
77
|
+
"Running a dev build (%s); not enforcing extension range %r.",
|
|
78
|
+
version,
|
|
79
|
+
spec,
|
|
80
|
+
)
|
|
81
|
+
return True
|
|
82
|
+
parsed = parse_range(spec)
|
|
83
|
+
if parsed is None:
|
|
84
|
+
return True
|
|
85
|
+
# prereleases=True so an rc of an in-range release still counts as in range.
|
|
86
|
+
return parsed.contains(running, prereleases=True)
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Discover active extensions across scopes into a resolved ExtensionSet."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from dataclasses import dataclass, replace
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import yaml
|
|
24
|
+
|
|
25
|
+
from graph_agents_cli.__init__ import __version__
|
|
26
|
+
from graph_agents_cli.extension._compat import is_compatible
|
|
27
|
+
from graph_agents_cli.extension._manifest import EXTENSIONS_FILE
|
|
28
|
+
from graph_agents_cli.extension._paths import vendored_extensions_dir
|
|
29
|
+
from graph_agents_cli.extension._resolver import ResolverError, validate_extension_name
|
|
30
|
+
from graph_agents_cli.extension._spec import (
|
|
31
|
+
EXTENSION_FILE,
|
|
32
|
+
ExtensionCommand,
|
|
33
|
+
ExtensionSpec,
|
|
34
|
+
ExtensionSpecError,
|
|
35
|
+
parse_extension_spec,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Commands no extension may override, ever. They are how a user removes or
|
|
39
|
+
# repairs an extension, so letting one own them means a broken extension can own
|
|
40
|
+
# its own uninstall. Refused at `extension add` and ignored here, rather than
|
|
41
|
+
# only when the extension is out of range: an in-range extension owning
|
|
42
|
+
# `extension remove` is the same trap.
|
|
43
|
+
RECOVERY_COMMANDS = frozenset(
|
|
44
|
+
{
|
|
45
|
+
"install",
|
|
46
|
+
"extension",
|
|
47
|
+
"extension.add",
|
|
48
|
+
"extension.list",
|
|
49
|
+
"extension.remove",
|
|
50
|
+
"extension.update",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class ResolvedCommand:
|
|
57
|
+
contribution: ExtensionCommand
|
|
58
|
+
extension_name: str
|
|
59
|
+
scope: str
|
|
60
|
+
extension_root: Path
|
|
61
|
+
# Set when the owning extension declared `on_incompatible: error` and the
|
|
62
|
+
# running CLI is outside its range. `main` installs a command that fails
|
|
63
|
+
# with an explanation instead of running the extension's vector — falling back
|
|
64
|
+
# to the built-in would silently do something else (an ADK scaffold in a
|
|
65
|
+
# LangChain project, say).
|
|
66
|
+
blocked_requires: str | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class ExtensionSet:
|
|
71
|
+
# Tuples (and frozen specs) because a set is parsed once and then read by
|
|
72
|
+
# main, info and extension list; no consumer may mutate it.
|
|
73
|
+
extensions: tuple[ExtensionSpec, ...]
|
|
74
|
+
commands: dict[str, ResolvedCommand]
|
|
75
|
+
conflicts: tuple[str, ...]
|
|
76
|
+
# Extensions whose declared range excludes `running_version`.
|
|
77
|
+
incompatible: tuple[ExtensionSpec, ...] = ()
|
|
78
|
+
running_version: str = __version__
|
|
79
|
+
|
|
80
|
+
def command_rows(self) -> list[dict]:
|
|
81
|
+
"""Contributed commands, one JSON-ready row each, sorted by command name.
|
|
82
|
+
|
|
83
|
+
A blocked row is a command the CLI will refuse to run (its extension is out
|
|
84
|
+
of range in `error` mode), so it is reported but not as active.
|
|
85
|
+
"""
|
|
86
|
+
requires_by_name = {
|
|
87
|
+
spec.name: spec.requires_agents_cli
|
|
88
|
+
for spec in self.extensions
|
|
89
|
+
if spec.requires_agents_cli
|
|
90
|
+
}
|
|
91
|
+
return [
|
|
92
|
+
{
|
|
93
|
+
"command": name,
|
|
94
|
+
"extension": rc.extension_name,
|
|
95
|
+
"scope": rc.scope,
|
|
96
|
+
"kind": rc.contribution.kind,
|
|
97
|
+
"run": list(rc.contribution.run),
|
|
98
|
+
"requires": requires_by_name.get(rc.extension_name),
|
|
99
|
+
"blocked": rc.blocked_requires is not None,
|
|
100
|
+
}
|
|
101
|
+
for name, rc in sorted(self.commands.items())
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
def conflict_rows(self) -> list[str]:
|
|
105
|
+
"""Command names claimed by more than one extension at one scope."""
|
|
106
|
+
return list(self.conflicts)
|
|
107
|
+
|
|
108
|
+
def incompatible_rows(self) -> list[dict]:
|
|
109
|
+
"""Out-of-range extensions, one JSON-ready row each."""
|
|
110
|
+
return [
|
|
111
|
+
{
|
|
112
|
+
"extension": spec.name,
|
|
113
|
+
"scope": spec.scope,
|
|
114
|
+
"requires": spec.requires_agents_cli,
|
|
115
|
+
"running": self.running_version,
|
|
116
|
+
"mode": "error" if spec.error_on_incompatible else "warn",
|
|
117
|
+
}
|
|
118
|
+
for spec in self.incompatible
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def incompatible_spec(extension_dir: Path) -> ExtensionSpec | None:
|
|
123
|
+
"""The spec if it is out of range for this CLI, else None.
|
|
124
|
+
|
|
125
|
+
Raises ExtensionSpecError if the manifest is there but unparseable; an
|
|
126
|
+
absent manifest, or one with no `requires`, returns None.
|
|
127
|
+
"""
|
|
128
|
+
spec = load_extension_spec(extension_dir)
|
|
129
|
+
if (
|
|
130
|
+
spec
|
|
131
|
+
and spec.requires_agents_cli
|
|
132
|
+
and not is_compatible(__version__, spec.requires_agents_cli)
|
|
133
|
+
):
|
|
134
|
+
return spec
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _load_yaml(path: Path) -> dict:
|
|
139
|
+
try:
|
|
140
|
+
with open(path, encoding="utf-8") as f:
|
|
141
|
+
data = yaml.safe_load(f) or {}
|
|
142
|
+
except (OSError, RecursionError, UnicodeDecodeError, yaml.YAMLError) as e:
|
|
143
|
+
logging.warning("%s: could not read (%s); ignoring.", path, e)
|
|
144
|
+
return {}
|
|
145
|
+
return data if isinstance(data, dict) else {}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def load_extension_spec(root: Path) -> ExtensionSpec | None:
|
|
149
|
+
"""Parse an extension's manifest.
|
|
150
|
+
|
|
151
|
+
None when there is no manifest; raises ExtensionSpecError when there is a bad one.
|
|
152
|
+
|
|
153
|
+
Everything on the load path catches it and carries on. `extension add` and
|
|
154
|
+
`extension update` let it surface, so an install fails instead of reporting
|
|
155
|
+
success for an extension that contributes nothing.
|
|
156
|
+
"""
|
|
157
|
+
path = root / EXTENSION_FILE
|
|
158
|
+
if not path.exists():
|
|
159
|
+
return None
|
|
160
|
+
try:
|
|
161
|
+
with open(path, encoding="utf-8") as f:
|
|
162
|
+
data = yaml.safe_load(f)
|
|
163
|
+
except (OSError, RecursionError, UnicodeDecodeError, yaml.YAMLError) as e:
|
|
164
|
+
# Unreadable, undecodable and unparseable are all as fatal as each other
|
|
165
|
+
# for `add`, and all as skippable for the load path, so they have to
|
|
166
|
+
# arrive as the same error type. RecursionError covers pathological
|
|
167
|
+
# nesting, which yaml raises straight through.
|
|
168
|
+
raise ExtensionSpecError(f"could not be read or parsed: {e}") from e
|
|
169
|
+
if data is not None and not isinstance(data, dict):
|
|
170
|
+
raise ExtensionSpecError(f"expected a mapping, found {type(data).__name__}")
|
|
171
|
+
return parse_extension_spec(data or {}, root=root, default_name=root.name)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _loaded_spec(root: Path) -> ExtensionSpec | None:
|
|
175
|
+
"""`load_extension_spec`, fail-soft: a bad manifest warns and is skipped."""
|
|
176
|
+
try:
|
|
177
|
+
return load_extension_spec(root)
|
|
178
|
+
except ExtensionSpecError as e:
|
|
179
|
+
logging.warning("%s: %s; ignoring this extension.", root / EXTENSION_FILE, e)
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _vendored_specs(scope_root: Path) -> list[ExtensionSpec]:
|
|
184
|
+
"""Load specs for extensions recorded under `extensions:` and vendored on disk."""
|
|
185
|
+
manifest = scope_root / EXTENSIONS_FILE
|
|
186
|
+
if not manifest.exists():
|
|
187
|
+
return []
|
|
188
|
+
data = _load_yaml(manifest)
|
|
189
|
+
raw = data.get("extensions") or []
|
|
190
|
+
specs: list[ExtensionSpec] = []
|
|
191
|
+
if not isinstance(raw, list):
|
|
192
|
+
return specs
|
|
193
|
+
for item in raw:
|
|
194
|
+
if not isinstance(item, dict) or not isinstance(item.get("name"), str):
|
|
195
|
+
continue
|
|
196
|
+
try:
|
|
197
|
+
safe_name = validate_extension_name(item["name"])
|
|
198
|
+
except ResolverError as e:
|
|
199
|
+
logging.warning("Skipping extension with %s", e)
|
|
200
|
+
continue
|
|
201
|
+
extension_dir = vendored_extensions_dir(scope_root) / safe_name
|
|
202
|
+
try:
|
|
203
|
+
spec = load_extension_spec(extension_dir)
|
|
204
|
+
except ExtensionSpecError as e:
|
|
205
|
+
logging.warning("%s: %s; ignoring this extension.", extension_dir, e)
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
if spec is None:
|
|
209
|
+
logging.warning(
|
|
210
|
+
"Extension %r is recorded in %s but not vendored at %s; "
|
|
211
|
+
"run `graph-agents-cli install`.",
|
|
212
|
+
item["name"],
|
|
213
|
+
manifest,
|
|
214
|
+
extension_dir,
|
|
215
|
+
)
|
|
216
|
+
continue
|
|
217
|
+
# The manifest id is the extension's stable identity (used by `extension
|
|
218
|
+
# remove`/`update`); keep it even if the upstream yaml `name` was
|
|
219
|
+
# renamed after the extension was pinned.
|
|
220
|
+
specs.append(replace(spec, name=safe_name))
|
|
221
|
+
return specs
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def load_extension_set(project_root: Path | None, user_root: Path | None) -> ExtensionSet:
|
|
225
|
+
# Lowest precedence first (user), highest last (project), so project-scope
|
|
226
|
+
# entries win by overwriting user-scope ones in the loop below.
|
|
227
|
+
discovered: list[ExtensionSpec] = []
|
|
228
|
+
for root, scope in ((user_root, "user"), (project_root, "project")):
|
|
229
|
+
if root is None:
|
|
230
|
+
continue
|
|
231
|
+
inline = _loaded_spec(root)
|
|
232
|
+
if inline is not None:
|
|
233
|
+
discovered.append(replace(inline, scope=scope))
|
|
234
|
+
for spec in _vendored_specs(root):
|
|
235
|
+
discovered.append(replace(spec, scope=scope))
|
|
236
|
+
|
|
237
|
+
commands: dict[str, ResolvedCommand] = {}
|
|
238
|
+
# (scope, command_name) -> extension_name that first claimed it at that scope.
|
|
239
|
+
claimed_same_scope: dict[tuple[str, str], str] = {}
|
|
240
|
+
conflicts: list[str] = []
|
|
241
|
+
incompatible: list[ExtensionSpec] = []
|
|
242
|
+
|
|
243
|
+
for spec in discovered:
|
|
244
|
+
scope = spec.scope
|
|
245
|
+
# An extension whose declared `requires.agents_cli` range excludes the
|
|
246
|
+
# running CLI is recorded here. "warn" applies its contributions anyway;
|
|
247
|
+
# "error" keeps the command claimed but marks it blocked, so invoking it
|
|
248
|
+
# fails with an explanation rather than silently running the built-in,
|
|
249
|
+
# which for an override does something else entirely.
|
|
250
|
+
blocked_requires: str | None = None
|
|
251
|
+
if spec.requires_agents_cli and not is_compatible(__version__, spec.requires_agents_cli):
|
|
252
|
+
incompatible.append(spec)
|
|
253
|
+
if spec.error_on_incompatible:
|
|
254
|
+
blocked_requires = spec.requires_agents_cli
|
|
255
|
+
for contribution in spec.commands:
|
|
256
|
+
key = contribution.name
|
|
257
|
+
same_scope_key = (scope, key)
|
|
258
|
+
if same_scope_key in claimed_same_scope:
|
|
259
|
+
conflicts.append(f"{scope}:{key}")
|
|
260
|
+
logging.warning(
|
|
261
|
+
"Command %r claimed by multiple %s-scope extensions "
|
|
262
|
+
"(%s and %s); ignoring the later one.",
|
|
263
|
+
key,
|
|
264
|
+
scope,
|
|
265
|
+
claimed_same_scope[same_scope_key],
|
|
266
|
+
spec.name,
|
|
267
|
+
)
|
|
268
|
+
continue
|
|
269
|
+
claimed_same_scope[same_scope_key] = spec.name
|
|
270
|
+
if key in RECOVERY_COMMANDS:
|
|
271
|
+
# `extension add` refuses these, so reaching here means a
|
|
272
|
+
# hand-edited manifest or one installed by an older CLI.
|
|
273
|
+
logging.warning(
|
|
274
|
+
"Extension %r claims %r, which cannot be overridden (it is how "
|
|
275
|
+
"you repair or remove an extension); using the built-in.",
|
|
276
|
+
spec.name,
|
|
277
|
+
key,
|
|
278
|
+
)
|
|
279
|
+
continue
|
|
280
|
+
commands[key] = ResolvedCommand(
|
|
281
|
+
contribution=contribution,
|
|
282
|
+
extension_name=spec.name,
|
|
283
|
+
scope=scope,
|
|
284
|
+
extension_root=spec.root,
|
|
285
|
+
blocked_requires=blocked_requires,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
return ExtensionSet(
|
|
289
|
+
extensions=tuple(discovered),
|
|
290
|
+
commands=commands,
|
|
291
|
+
conflicts=tuple(conflicts),
|
|
292
|
+
incompatible=tuple(incompatible),
|
|
293
|
+
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Read/write the installed-extension list.
|
|
16
|
+
|
|
17
|
+
Its own file rather than a block in graph-agents-cli-manifest.yaml: this is
|
|
18
|
+
read-modify-written on every add/remove/update, and a bug here should not be
|
|
19
|
+
able to reach a project's scaffold state. Same filename at both scopes, under
|
|
20
|
+
the project root or the user config dir, so there is nothing to branch on.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
import yaml
|
|
31
|
+
|
|
32
|
+
EXTENSIONS_FILE = "graph-agents-cli-extensions.yaml"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class ExtensionEntry:
|
|
37
|
+
name: str
|
|
38
|
+
source: str
|
|
39
|
+
ref: str
|
|
40
|
+
sha: str
|
|
41
|
+
scope: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _read_manifest(path: Path) -> dict[str, Any]:
|
|
45
|
+
if not path.exists():
|
|
46
|
+
return {}
|
|
47
|
+
try:
|
|
48
|
+
with open(path, encoding="utf-8") as f:
|
|
49
|
+
raw = yaml.safe_load(f)
|
|
50
|
+
except yaml.YAMLError as e:
|
|
51
|
+
# add/remove/update and install's sync all read this; a hand-edited
|
|
52
|
+
# file should not end them in a traceback.
|
|
53
|
+
logging.warning("Ignoring unparseable %s: %s", path, e)
|
|
54
|
+
return {}
|
|
55
|
+
if isinstance(raw, dict):
|
|
56
|
+
return raw # type: ignore[return-value]
|
|
57
|
+
return {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _write_manifest(path: Path, data: dict[str, Any]) -> None:
|
|
61
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
63
|
+
yaml.safe_dump(data, f, sort_keys=False)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_extension_entries(manifest_path: Path) -> list[ExtensionEntry]:
|
|
67
|
+
"""Read extension entries from a manifest file.
|
|
68
|
+
|
|
69
|
+
Returns an empty list when the file is missing, unparseable, or has no
|
|
70
|
+
valid ``extensions:`` list.
|
|
71
|
+
"""
|
|
72
|
+
data = _read_manifest(manifest_path)
|
|
73
|
+
raw = data.get("extensions") or []
|
|
74
|
+
entries: list[ExtensionEntry] = []
|
|
75
|
+
if not isinstance(raw, list):
|
|
76
|
+
return entries
|
|
77
|
+
for item in raw:
|
|
78
|
+
if not isinstance(item, dict):
|
|
79
|
+
logging.warning(
|
|
80
|
+
"%s: ignoring malformed `extensions:` entry %r (expected a mapping).",
|
|
81
|
+
manifest_path,
|
|
82
|
+
item,
|
|
83
|
+
)
|
|
84
|
+
continue
|
|
85
|
+
entries.append(
|
|
86
|
+
ExtensionEntry(
|
|
87
|
+
name=str(item.get("name") or ""),
|
|
88
|
+
source=str(item.get("source") or ""),
|
|
89
|
+
ref=str(item.get("ref") or ""),
|
|
90
|
+
sha=str(item.get("sha") or ""),
|
|
91
|
+
scope=str(item.get("scope") or ""),
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
return entries
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def upsert_extension_entry(manifest_path: Path, entry: ExtensionEntry) -> None:
|
|
98
|
+
"""Insert or replace an extension entry in the manifest, preserving all other keys.
|
|
99
|
+
|
|
100
|
+
Creates the manifest file (and any missing parent directories) if absent.
|
|
101
|
+
"""
|
|
102
|
+
data = _read_manifest(manifest_path)
|
|
103
|
+
existing: list[Any] = data.get("extensions") or []
|
|
104
|
+
# Drop only the entry being replaced. Anything we don't recognise is kept
|
|
105
|
+
# verbatim — rewriting the manifest must not delete a user's content.
|
|
106
|
+
extensions: list[Any] = [
|
|
107
|
+
p for p in existing if not (isinstance(p, dict) and p.get("name") == entry.name)
|
|
108
|
+
]
|
|
109
|
+
extensions.append(
|
|
110
|
+
{
|
|
111
|
+
"name": entry.name,
|
|
112
|
+
"source": entry.source,
|
|
113
|
+
"ref": entry.ref,
|
|
114
|
+
"sha": entry.sha,
|
|
115
|
+
"scope": entry.scope,
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
data["extensions"] = extensions
|
|
119
|
+
_write_manifest(manifest_path, data)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def remove_extension_entry(manifest_path: Path, name: str) -> bool:
|
|
123
|
+
"""Remove an extension entry by name.
|
|
124
|
+
|
|
125
|
+
Returns True if an entry was removed, False if no entry matched.
|
|
126
|
+
"""
|
|
127
|
+
data = _read_manifest(manifest_path)
|
|
128
|
+
existing: list[Any] = data.get("extensions") or []
|
|
129
|
+
# Same rule as upsert: unrecognised entries survive, only `name` matches go.
|
|
130
|
+
kept: list[Any] = [p for p in existing if not (isinstance(p, dict) and p.get("name") == name)]
|
|
131
|
+
if len(kept) == len(existing):
|
|
132
|
+
return False
|
|
133
|
+
data["extensions"] = kept
|
|
134
|
+
_write_manifest(manifest_path, data)
|
|
135
|
+
return True
|