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,894 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
# Modifications Copyright 2026 graph-agents-cli contributors
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""3-way file comparison and dependency merging for upgrade and enhance."""
|
|
17
|
+
|
|
18
|
+
import fnmatch
|
|
19
|
+
import hashlib
|
|
20
|
+
import logging
|
|
21
|
+
import pathlib
|
|
22
|
+
import re
|
|
23
|
+
import tomllib
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Any, Literal
|
|
27
|
+
|
|
28
|
+
import yaml
|
|
29
|
+
|
|
30
|
+
MANIFEST_FILENAME = "graph-agents-cli-manifest.yaml"
|
|
31
|
+
|
|
32
|
+
# Preservation rules: what upgrade/enhance never overwrite. Patterns use the
|
|
33
|
+
# {agent_directory} placeholder, replaced at runtime. Everything not listed is
|
|
34
|
+
# "scaffolding" and gets the 3-way compare.
|
|
35
|
+
FILE_CATEGORIES: dict[str, list[str]] = {
|
|
36
|
+
"agent_code": [ # Never modified
|
|
37
|
+
"{agent_directory}/agent.py",
|
|
38
|
+
"{agent_directory}/tools/**",
|
|
39
|
+
"{agent_directory}/policies/**",
|
|
40
|
+
# The shape of the agent's final answer (structured answers), when it has one.
|
|
41
|
+
"{agent_directory}/response_schema.json",
|
|
42
|
+
# Reserved for directories the developer may create.
|
|
43
|
+
"{agent_directory}/prompts/**",
|
|
44
|
+
"{agent_directory}/graph/**",
|
|
45
|
+
],
|
|
46
|
+
"config_files": [ # Never overwritten
|
|
47
|
+
".env",
|
|
48
|
+
".env.*",
|
|
49
|
+
"api-policy.yaml",
|
|
50
|
+
"deployment/helm/*/values-*.yaml",
|
|
51
|
+
"deployment/argocd/**",
|
|
52
|
+
"tests/eval/datasets/**",
|
|
53
|
+
"tests/eval/eval_config.yaml",
|
|
54
|
+
],
|
|
55
|
+
"dependencies": [ # Semantic merge
|
|
56
|
+
"pyproject.toml",
|
|
57
|
+
MANIFEST_FILENAME,
|
|
58
|
+
],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Config files a runtime, model provider, target or CD change re-renders. Under
|
|
63
|
+
# ``scaffold enhance`` (``merge_config=True``) they are merged three-way instead
|
|
64
|
+
# of skipped (config files) or kept whole on the first edit (the chart's
|
|
65
|
+
# values.yaml, .github/agent.env): an untouched copy takes the template's new
|
|
66
|
+
# version, an edited one gets the template's change applied around the
|
|
67
|
+
# developer's edits (line by line, else key by key), and a key the developer
|
|
68
|
+
# changed too is left alone and reported. ``.env`` (secrets), ``api-policy.yaml``
|
|
69
|
+
# and the eval datasets never are: an enhance does not change them.
|
|
70
|
+
STRUCTURAL_CONFIG_FILES: list[str] = [
|
|
71
|
+
".env.example",
|
|
72
|
+
".github/agent.env",
|
|
73
|
+
"deployment/helm/*/values.yaml",
|
|
74
|
+
"deployment/helm/*/values-*.yaml",
|
|
75
|
+
"deployment/argocd/**",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
# Files that define the container image. Under enhance, a template change to
|
|
79
|
+
# one of them that could not be applied leaves the image on the old settings
|
|
80
|
+
# (the fastapi image under the langgraph-server runtime, say), so the project
|
|
81
|
+
# does not build what the manifest says until the developer merges it by hand.
|
|
82
|
+
IMAGE_DEFINITION_FILES: list[str] = ["Dockerfile"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class Followup:
|
|
87
|
+
"""Something the developer still has to do by hand after upgrade/enhance.
|
|
88
|
+
|
|
89
|
+
``required``: until it is done the project does not build or deploy with
|
|
90
|
+
the settings the manifest records (enhance then exits non-zero).
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
text: str
|
|
94
|
+
required: bool = False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# Preserve type literals for type-safe reason matching
|
|
98
|
+
PreserveType = Literal["gacli_unchanged", "already_current", "unchanged_both", None]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class FileCompareResult:
|
|
103
|
+
"""Result of comparing a file across three versions."""
|
|
104
|
+
|
|
105
|
+
path: str
|
|
106
|
+
category: str
|
|
107
|
+
action: Literal["auto_update", "preserve", "skip", "conflict", "new", "removed", "merge"]
|
|
108
|
+
reason: str
|
|
109
|
+
# For preserve actions, indicates why preserved
|
|
110
|
+
preserve_type: PreserveType = None
|
|
111
|
+
# For conflicts, store the content hashes
|
|
112
|
+
current_hash: str | None = None
|
|
113
|
+
old_template_hash: str | None = None
|
|
114
|
+
new_template_hash: str | None = None
|
|
115
|
+
# Something the developer still has to do for this file (printed after the run).
|
|
116
|
+
followup: str | Followup | None = None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class DependencyReadError(Exception):
|
|
120
|
+
"""A dependency manifest could not be read: it's missing or unparseable."""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class DependencyResolution:
|
|
125
|
+
"""What the merge decided to do with one dependency.
|
|
126
|
+
|
|
127
|
+
Statuses:
|
|
128
|
+
- "added": the template introduced this dependency.
|
|
129
|
+
- "updated": a template-managed dependency whose spec the template changed.
|
|
130
|
+
- "removed": a template-managed dependency the new template dropped.
|
|
131
|
+
- "unchanged": a template-managed dependency the template left as-is.
|
|
132
|
+
- "kept": a dependency *the user* added, preserved untouched.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
name: str
|
|
136
|
+
status: Literal["updated", "added", "removed", "kept", "unchanged"]
|
|
137
|
+
old_version: str | None = None
|
|
138
|
+
new_version: str | None = None
|
|
139
|
+
|
|
140
|
+
def full_name(self) -> str:
|
|
141
|
+
"""The full requirement to write: ``name[extra]version``."""
|
|
142
|
+
return f"{self.name}{self.new_version or ''}"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _expand_patterns(patterns: list[str], agent_directory: str) -> list[str]:
|
|
146
|
+
"""Expand {agent_directory} placeholder in patterns."""
|
|
147
|
+
return [p.replace("{agent_directory}", agent_directory) for p in patterns]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _matches_any_pattern(path: str, patterns: list[str]) -> bool:
|
|
151
|
+
"""Check if path matches any glob pattern, including ** recursive patterns."""
|
|
152
|
+
path = path.replace("\\", "/")
|
|
153
|
+
|
|
154
|
+
for pattern in patterns:
|
|
155
|
+
pattern = pattern.replace("\\", "/")
|
|
156
|
+
|
|
157
|
+
if fnmatch.fnmatch(path, pattern):
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
if "**" in pattern:
|
|
161
|
+
regex = re.escape(pattern)
|
|
162
|
+
regex = regex.replace(r"\*\*/", "(?:.*/)?") # **/ = zero or more dirs
|
|
163
|
+
regex = regex.replace(r"\*\*", ".*")
|
|
164
|
+
regex = regex.replace(r"\*", "[^/]*")
|
|
165
|
+
if re.match(f"^{regex}$", path):
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def is_structural_config(path: str) -> bool:
|
|
172
|
+
"""True for a config file the enhanced settings re-render (``STRUCTURAL_CONFIG_FILES``)."""
|
|
173
|
+
return _matches_any_pattern(path, STRUCTURAL_CONFIG_FILES)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def is_image_definition(path: str) -> bool:
|
|
177
|
+
"""True for a file that defines the container image (``IMAGE_DEFINITION_FILES``)."""
|
|
178
|
+
return _matches_any_pattern(path, IMAGE_DEFINITION_FILES)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def categorize_file(path: str, agent_directory: str = "app") -> str:
|
|
182
|
+
"""Return category: agent_code, config_files, dependencies, or scaffolding."""
|
|
183
|
+
for category, patterns in FILE_CATEGORIES.items():
|
|
184
|
+
expanded = _expand_patterns(patterns, agent_directory)
|
|
185
|
+
if _matches_any_pattern(path, expanded):
|
|
186
|
+
return category
|
|
187
|
+
return "scaffolding"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _file_hash(file_path: pathlib.Path) -> str | None:
|
|
191
|
+
"""Calculate SHA256 hash of a file's contents."""
|
|
192
|
+
if not file_path.exists():
|
|
193
|
+
return None
|
|
194
|
+
try:
|
|
195
|
+
content = file_path.read_bytes()
|
|
196
|
+
return hashlib.sha256(content).hexdigest()
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logging.warning(f"Could not hash file {file_path}: {e}")
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def three_way_compare(
|
|
203
|
+
relative_path: str,
|
|
204
|
+
project_dir: pathlib.Path,
|
|
205
|
+
old_template_dir: pathlib.Path,
|
|
206
|
+
new_template_dir: pathlib.Path,
|
|
207
|
+
agent_directory: str = "app",
|
|
208
|
+
*,
|
|
209
|
+
merge_config: bool = False,
|
|
210
|
+
) -> FileCompareResult:
|
|
211
|
+
"""Compare file across current, old template, and new template.
|
|
212
|
+
|
|
213
|
+
Returns action based on:
|
|
214
|
+
- current == old -> auto-update (user didn't modify)
|
|
215
|
+
- old == new -> preserve (the CLI didn't change)
|
|
216
|
+
- all differ -> conflict
|
|
217
|
+
|
|
218
|
+
``merge_config`` (enhance) gives the ``STRUCTURAL_CONFIG_FILES`` a three-way
|
|
219
|
+
merge instead of the config-file skip.
|
|
220
|
+
"""
|
|
221
|
+
category = categorize_file(relative_path, agent_directory)
|
|
222
|
+
|
|
223
|
+
current_file = project_dir / relative_path
|
|
224
|
+
old_template_file = old_template_dir / relative_path
|
|
225
|
+
new_template_file = new_template_dir / relative_path
|
|
226
|
+
|
|
227
|
+
if category in ("agent_code", "config_files") and not current_file.exists():
|
|
228
|
+
# Never modified / never overwritten: a file the project does not have
|
|
229
|
+
# yet is added only when the old snapshot did not ship it either (e.g.
|
|
230
|
+
# the argocd Applications that `enhance --cd argocd` introduces). When
|
|
231
|
+
# the old template shipped it and the project no longer has it, the
|
|
232
|
+
# developer removed it deliberately: it is never re-added.
|
|
233
|
+
new_hash = _file_hash(new_template_file)
|
|
234
|
+
if new_hash is not None:
|
|
235
|
+
if old_template_file.exists():
|
|
236
|
+
return FileCompareResult(
|
|
237
|
+
path=relative_path,
|
|
238
|
+
category=category,
|
|
239
|
+
action="skip",
|
|
240
|
+
reason="Removed by you (never re-added)",
|
|
241
|
+
new_template_hash=new_hash,
|
|
242
|
+
)
|
|
243
|
+
return FileCompareResult(
|
|
244
|
+
path=relative_path,
|
|
245
|
+
category=category,
|
|
246
|
+
action="new",
|
|
247
|
+
reason="New file in the template (added, then never touched again)",
|
|
248
|
+
new_template_hash=new_hash,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if category == "agent_code":
|
|
252
|
+
return FileCompareResult(
|
|
253
|
+
path=relative_path,
|
|
254
|
+
category=category,
|
|
255
|
+
action="skip",
|
|
256
|
+
reason="Agent code (never modified by upgrade)",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if (
|
|
260
|
+
merge_config
|
|
261
|
+
and category in ("config_files", "scaffolding")
|
|
262
|
+
and is_structural_config(relative_path)
|
|
263
|
+
):
|
|
264
|
+
return _compare_structural_config(
|
|
265
|
+
relative_path, current_file, old_template_file, new_template_file, category
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if category == "config_files":
|
|
269
|
+
return FileCompareResult(
|
|
270
|
+
path=relative_path,
|
|
271
|
+
category=category,
|
|
272
|
+
action="skip",
|
|
273
|
+
reason="Config file (user's environment settings)",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if category == "dependencies":
|
|
277
|
+
return FileCompareResult(
|
|
278
|
+
path=relative_path,
|
|
279
|
+
category=category,
|
|
280
|
+
action="preserve",
|
|
281
|
+
reason="Dependencies (requires merge handling)",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
current_hash = _file_hash(current_file)
|
|
285
|
+
old_hash = _file_hash(old_template_file)
|
|
286
|
+
new_hash = _file_hash(new_template_file)
|
|
287
|
+
|
|
288
|
+
# New file in the template (not in project, regardless of old template)
|
|
289
|
+
if current_hash is None and new_hash is not None:
|
|
290
|
+
return FileCompareResult(
|
|
291
|
+
path=relative_path,
|
|
292
|
+
category=category,
|
|
293
|
+
action="new",
|
|
294
|
+
reason="New file in the template",
|
|
295
|
+
new_template_hash=new_hash,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# File removed in new template
|
|
299
|
+
if current_hash is not None and old_hash is not None and new_hash is None:
|
|
300
|
+
if current_hash == old_hash:
|
|
301
|
+
return FileCompareResult(
|
|
302
|
+
path=relative_path,
|
|
303
|
+
category=category,
|
|
304
|
+
action="removed",
|
|
305
|
+
reason="File removed from the template (you didn't modify it)",
|
|
306
|
+
current_hash=current_hash,
|
|
307
|
+
old_template_hash=old_hash,
|
|
308
|
+
)
|
|
309
|
+
return FileCompareResult(
|
|
310
|
+
path=relative_path,
|
|
311
|
+
category=category,
|
|
312
|
+
action="conflict",
|
|
313
|
+
reason="File removed from the template but you modified it",
|
|
314
|
+
current_hash=current_hash,
|
|
315
|
+
old_template_hash=old_hash,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# File only in current project (user-added, not part of the template)
|
|
319
|
+
if current_hash is not None and old_hash is None and new_hash is None:
|
|
320
|
+
return FileCompareResult(
|
|
321
|
+
path=relative_path,
|
|
322
|
+
category=category,
|
|
323
|
+
action="skip",
|
|
324
|
+
reason="User-added file (not part of the template)",
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# File doesn't exist anywhere relevant
|
|
328
|
+
if current_hash is None and new_hash is None:
|
|
329
|
+
return FileCompareResult(
|
|
330
|
+
path=relative_path,
|
|
331
|
+
category=category,
|
|
332
|
+
action="skip",
|
|
333
|
+
reason="File not present",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# User didn't modify (current == old)
|
|
337
|
+
if current_hash == old_hash and new_hash is not None:
|
|
338
|
+
if old_hash == new_hash:
|
|
339
|
+
return FileCompareResult(
|
|
340
|
+
path=relative_path,
|
|
341
|
+
category=category,
|
|
342
|
+
action="preserve",
|
|
343
|
+
reason="Unchanged in both project and template",
|
|
344
|
+
preserve_type="unchanged_both",
|
|
345
|
+
current_hash=current_hash,
|
|
346
|
+
old_template_hash=old_hash,
|
|
347
|
+
new_template_hash=new_hash,
|
|
348
|
+
)
|
|
349
|
+
return FileCompareResult(
|
|
350
|
+
path=relative_path,
|
|
351
|
+
category=category,
|
|
352
|
+
action="auto_update",
|
|
353
|
+
reason="You didn't modify this file",
|
|
354
|
+
current_hash=current_hash,
|
|
355
|
+
old_template_hash=old_hash,
|
|
356
|
+
new_template_hash=new_hash,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# The template didn't change (old == new)
|
|
360
|
+
if old_hash == new_hash and current_hash is not None:
|
|
361
|
+
return FileCompareResult(
|
|
362
|
+
path=relative_path,
|
|
363
|
+
category=category,
|
|
364
|
+
action="preserve",
|
|
365
|
+
reason="The template didn't change this file",
|
|
366
|
+
preserve_type="gacli_unchanged",
|
|
367
|
+
current_hash=current_hash,
|
|
368
|
+
old_template_hash=old_hash,
|
|
369
|
+
new_template_hash=new_hash,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Already up to date (current == new)
|
|
373
|
+
if current_hash == new_hash:
|
|
374
|
+
return FileCompareResult(
|
|
375
|
+
path=relative_path,
|
|
376
|
+
category=category,
|
|
377
|
+
action="preserve",
|
|
378
|
+
reason="Already up to date",
|
|
379
|
+
preserve_type="already_current",
|
|
380
|
+
current_hash=current_hash,
|
|
381
|
+
old_template_hash=old_hash,
|
|
382
|
+
new_template_hash=new_hash,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# All three differ -> conflict
|
|
386
|
+
return FileCompareResult(
|
|
387
|
+
path=relative_path,
|
|
388
|
+
category=category,
|
|
389
|
+
action="conflict",
|
|
390
|
+
reason="Both you and the template modified this file",
|
|
391
|
+
current_hash=current_hash,
|
|
392
|
+
old_template_hash=old_hash,
|
|
393
|
+
new_template_hash=new_hash,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _read_text(path: pathlib.Path) -> str | None:
|
|
398
|
+
try:
|
|
399
|
+
return path.read_text(encoding="utf-8")
|
|
400
|
+
except (OSError, UnicodeDecodeError):
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _compare_structural_config(
|
|
405
|
+
relative_path: str,
|
|
406
|
+
current_file: pathlib.Path,
|
|
407
|
+
old_template_file: pathlib.Path,
|
|
408
|
+
new_template_file: pathlib.Path,
|
|
409
|
+
category: str,
|
|
410
|
+
) -> FileCompareResult:
|
|
411
|
+
"""Three-way handling of a config file the enhanced settings re-render (see STRUCTURAL_CONFIG_FILES)."""
|
|
412
|
+
from .merge3 import template_diff
|
|
413
|
+
|
|
414
|
+
current_hash = _file_hash(current_file)
|
|
415
|
+
old_hash = _file_hash(old_template_file)
|
|
416
|
+
new_hash = _file_hash(new_template_file)
|
|
417
|
+
hashes = {
|
|
418
|
+
"current_hash": current_hash,
|
|
419
|
+
"old_template_hash": old_hash,
|
|
420
|
+
"new_template_hash": new_hash,
|
|
421
|
+
}
|
|
422
|
+
if current_hash is None and new_hash is None:
|
|
423
|
+
return FileCompareResult(
|
|
424
|
+
path=relative_path,
|
|
425
|
+
category=category,
|
|
426
|
+
action="skip",
|
|
427
|
+
reason="File not present",
|
|
428
|
+
**hashes,
|
|
429
|
+
)
|
|
430
|
+
if current_hash is None and category == "scaffolding":
|
|
431
|
+
# Like any scaffolding file the project lacks (config files are handled
|
|
432
|
+
# before this: one the developer removed is never re-added).
|
|
433
|
+
return FileCompareResult(
|
|
434
|
+
path=relative_path,
|
|
435
|
+
category=category,
|
|
436
|
+
action="new",
|
|
437
|
+
reason="New file in the template",
|
|
438
|
+
**hashes,
|
|
439
|
+
)
|
|
440
|
+
if old_hash == new_hash:
|
|
441
|
+
return FileCompareResult(
|
|
442
|
+
path=relative_path,
|
|
443
|
+
category=category,
|
|
444
|
+
action="skip",
|
|
445
|
+
reason="Config file (not changed by the new settings)",
|
|
446
|
+
**hashes,
|
|
447
|
+
)
|
|
448
|
+
if new_hash is None and category == "scaffolding" and current_hash is not None:
|
|
449
|
+
# Gone with the rest of what the new settings no longer render (the whole
|
|
450
|
+
# chart when the target leaves kubernetes), as any scaffolding file.
|
|
451
|
+
if current_hash == old_hash:
|
|
452
|
+
return FileCompareResult(
|
|
453
|
+
path=relative_path,
|
|
454
|
+
category=category,
|
|
455
|
+
action="removed",
|
|
456
|
+
reason="File removed from the template (you didn't modify it)",
|
|
457
|
+
**hashes,
|
|
458
|
+
)
|
|
459
|
+
return FileCompareResult(
|
|
460
|
+
path=relative_path,
|
|
461
|
+
category=category,
|
|
462
|
+
action="conflict",
|
|
463
|
+
reason="File removed from the template but you modified it",
|
|
464
|
+
**hashes,
|
|
465
|
+
)
|
|
466
|
+
if new_hash is None:
|
|
467
|
+
return FileCompareResult(
|
|
468
|
+
path=relative_path,
|
|
469
|
+
category=category,
|
|
470
|
+
action="skip",
|
|
471
|
+
reason="Config file the new settings no longer render (kept)",
|
|
472
|
+
followup=(
|
|
473
|
+
f"{relative_path}: no longer part of the template for the new settings; it "
|
|
474
|
+
"was kept, delete it if nothing uses it any more"
|
|
475
|
+
),
|
|
476
|
+
**hashes,
|
|
477
|
+
)
|
|
478
|
+
if current_hash == new_hash:
|
|
479
|
+
return FileCompareResult(
|
|
480
|
+
path=relative_path,
|
|
481
|
+
category=category,
|
|
482
|
+
action="preserve",
|
|
483
|
+
reason="Already up to date",
|
|
484
|
+
preserve_type="already_current",
|
|
485
|
+
**hashes,
|
|
486
|
+
)
|
|
487
|
+
if current_hash == old_hash:
|
|
488
|
+
return FileCompareResult(
|
|
489
|
+
path=relative_path,
|
|
490
|
+
category=category,
|
|
491
|
+
action="auto_update",
|
|
492
|
+
reason="Config file you did not modify: updated for the new settings",
|
|
493
|
+
**hashes,
|
|
494
|
+
)
|
|
495
|
+
old_text = _read_text(old_template_file) if old_hash is not None else ""
|
|
496
|
+
current_text = _read_text(current_file)
|
|
497
|
+
new_text = _read_text(new_template_file)
|
|
498
|
+
merged, conflicts = (
|
|
499
|
+
(None, [])
|
|
500
|
+
if old_text is None or current_text is None or new_text is None
|
|
501
|
+
else _merge_config_texts(old_text, current_text, new_text, relative_path)
|
|
502
|
+
)
|
|
503
|
+
if merged is not None and merged != current_text:
|
|
504
|
+
return FileCompareResult(
|
|
505
|
+
path=relative_path,
|
|
506
|
+
category=category,
|
|
507
|
+
action="merge",
|
|
508
|
+
reason="Config file you modified: the template's change is merged around your edits",
|
|
509
|
+
followup=_kept_keys_followup(relative_path, conflicts, partly=True),
|
|
510
|
+
**hashes,
|
|
511
|
+
)
|
|
512
|
+
if merged is not None and conflicts:
|
|
513
|
+
return FileCompareResult(
|
|
514
|
+
path=relative_path,
|
|
515
|
+
category=category,
|
|
516
|
+
action="skip",
|
|
517
|
+
reason="Config file you modified where the template changed the same keys (kept)",
|
|
518
|
+
followup=_kept_keys_followup(relative_path, conflicts, partly=False),
|
|
519
|
+
**hashes,
|
|
520
|
+
)
|
|
521
|
+
if merged is not None:
|
|
522
|
+
# Every key already reads as the new settings want; only comments differ.
|
|
523
|
+
return FileCompareResult(
|
|
524
|
+
path=relative_path,
|
|
525
|
+
category=category,
|
|
526
|
+
action="preserve",
|
|
527
|
+
reason="Already up to date (your edits carry the new settings)",
|
|
528
|
+
preserve_type="already_current",
|
|
529
|
+
**hashes,
|
|
530
|
+
)
|
|
531
|
+
diff = template_diff(old_text or "", new_text or "", relative_path)
|
|
532
|
+
return FileCompareResult(
|
|
533
|
+
path=relative_path,
|
|
534
|
+
category=category,
|
|
535
|
+
action="skip",
|
|
536
|
+
reason="Config file you modified where the template changed the same lines (kept)",
|
|
537
|
+
followup=(
|
|
538
|
+
f"{relative_path}: your edits overlap the template's change for the new settings, "
|
|
539
|
+
"so the file was left as it is. Apply this change by hand:\n" + diff
|
|
540
|
+
),
|
|
541
|
+
**hashes,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _kept_keys_followup(relative_path: str, conflicts: list[Any], *, partly: bool) -> str | None:
|
|
546
|
+
"""The keys of ``relative_path`` the developer changed that the new settings change too."""
|
|
547
|
+
if not conflicts:
|
|
548
|
+
return None
|
|
549
|
+
head = (
|
|
550
|
+
"the new settings' change was applied except to keys you changed too"
|
|
551
|
+
if partly
|
|
552
|
+
else "left as it is, because you changed the keys the new settings change"
|
|
553
|
+
)
|
|
554
|
+
return (
|
|
555
|
+
f"{relative_path}: {head}. Your values were kept; change them by hand if they no "
|
|
556
|
+
"longer fit:\n" + "".join(f" {conflict.describe()}\n" for conflict in conflicts)
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _merge_config_texts(
|
|
561
|
+
old_text: str, current_text: str, new_text: str, relative_path: str
|
|
562
|
+
) -> tuple[str | None, list[Any]]:
|
|
563
|
+
"""The developer's config with the template's change applied, and the keys left alone.
|
|
564
|
+
|
|
565
|
+
The line merge first (it also carries the template's comment changes); when
|
|
566
|
+
the edits are too close for it, the key-by-key merge, which applies every
|
|
567
|
+
changed key the developer did not change too and returns the others.
|
|
568
|
+
``(None, [])`` when neither can merge the file safely.
|
|
569
|
+
"""
|
|
570
|
+
from .keymerge import merge_keys
|
|
571
|
+
from .merge3 import merge3_checked
|
|
572
|
+
|
|
573
|
+
merged = merge3_checked(old_text, current_text, new_text, relative_path)
|
|
574
|
+
if merged is not None:
|
|
575
|
+
return merged, []
|
|
576
|
+
result = merge_keys(old_text, current_text, new_text, relative_path)
|
|
577
|
+
if result is None:
|
|
578
|
+
return None, []
|
|
579
|
+
return result.text, list(result.conflicts)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def merged_config_text(
|
|
583
|
+
project_dir: pathlib.Path,
|
|
584
|
+
old_template_dir: pathlib.Path,
|
|
585
|
+
new_template_dir: pathlib.Path,
|
|
586
|
+
relative_path: str,
|
|
587
|
+
) -> str | None:
|
|
588
|
+
"""The three-way merge of a ``merge`` result, or None when it no longer merges cleanly."""
|
|
589
|
+
old_file = old_template_dir / relative_path
|
|
590
|
+
old_text = _read_text(old_file) if old_file.exists() else ""
|
|
591
|
+
current_text = _read_text(project_dir / relative_path)
|
|
592
|
+
new_text = _read_text(new_template_dir / relative_path)
|
|
593
|
+
if old_text is None or current_text is None or new_text is None:
|
|
594
|
+
return None
|
|
595
|
+
return _merge_config_texts(old_text, current_text, new_text, relative_path)[0]
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def collect_all_files(
|
|
599
|
+
project_dir: pathlib.Path,
|
|
600
|
+
old_template_dir: pathlib.Path,
|
|
601
|
+
new_template_dir: pathlib.Path,
|
|
602
|
+
exclude_patterns: list[str] | None = None,
|
|
603
|
+
) -> set[str]:
|
|
604
|
+
"""Collect all unique relative file paths from all three directories."""
|
|
605
|
+
if exclude_patterns is None:
|
|
606
|
+
exclude_patterns = [
|
|
607
|
+
".git/**",
|
|
608
|
+
".venv/**",
|
|
609
|
+
"venv/**",
|
|
610
|
+
"__pycache__/**",
|
|
611
|
+
"*.pyc",
|
|
612
|
+
".DS_Store",
|
|
613
|
+
"*.egg-info/**",
|
|
614
|
+
"uv.lock",
|
|
615
|
+
".uv/**",
|
|
616
|
+
".graph-agents-cli/**",
|
|
617
|
+
]
|
|
618
|
+
|
|
619
|
+
all_files: set[str] = set()
|
|
620
|
+
|
|
621
|
+
for base_dir in [project_dir, old_template_dir, new_template_dir]:
|
|
622
|
+
if not base_dir.exists():
|
|
623
|
+
continue
|
|
624
|
+
for file_path in base_dir.rglob("*"):
|
|
625
|
+
if file_path.is_file():
|
|
626
|
+
relative = str(file_path.relative_to(base_dir))
|
|
627
|
+
if not _matches_any_pattern(relative, exclude_patterns):
|
|
628
|
+
all_files.add(relative)
|
|
629
|
+
|
|
630
|
+
return all_files
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _parse_dependency(dep_str: str) -> tuple[str, str, str]:
|
|
634
|
+
"""Parse a dependency string into (base_name, extras, version_spec).
|
|
635
|
+
|
|
636
|
+
Examples:
|
|
637
|
+
"langgraph>=1.0" -> ("langgraph", "", ">=1.0")
|
|
638
|
+
"a2a-sdk[http-server]>=1.0" -> ("a2a-sdk", "[http-server]", ">=1.0")
|
|
639
|
+
"pytest" -> ("pytest", "", "")
|
|
640
|
+
"""
|
|
641
|
+
match = re.match(r"^([a-zA-Z0-9_-]+)(\[[^\]]+\])?(.*)", dep_str.strip())
|
|
642
|
+
if match:
|
|
643
|
+
base_name = match.group(1).lower()
|
|
644
|
+
extras = match.group(2) or ""
|
|
645
|
+
version = match.group(3).strip()
|
|
646
|
+
return base_name, extras, version
|
|
647
|
+
return dep_str.lower(), "", ""
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _load_dependencies_from_pyproject(pyproject_path: pathlib.Path) -> dict[str, str]:
|
|
651
|
+
"""Load dependencies as {base_name: version_spec} dict.
|
|
652
|
+
|
|
653
|
+
Raises DependencyReadError if pyproject.toml is missing or unparseable; a
|
|
654
|
+
present file with no dependencies yields {}.
|
|
655
|
+
"""
|
|
656
|
+
if not pyproject_path.exists():
|
|
657
|
+
raise DependencyReadError(f"Dependency manifest not found: {pyproject_path}")
|
|
658
|
+
|
|
659
|
+
try:
|
|
660
|
+
with open(pyproject_path, "rb") as f:
|
|
661
|
+
data = tomllib.load(f)
|
|
662
|
+
|
|
663
|
+
deps = data.get("project", {}).get("dependencies", [])
|
|
664
|
+
result: dict[str, str] = {}
|
|
665
|
+
for dep in deps:
|
|
666
|
+
name, extras, version = _parse_dependency(dep)
|
|
667
|
+
result[name] = f"{extras}{version}"
|
|
668
|
+
return result
|
|
669
|
+
except Exception as e:
|
|
670
|
+
raise DependencyReadError(f"Could not read dependencies from {pyproject_path}: {e}") from e
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _resolve_dependencies(
|
|
674
|
+
current: dict[str, str],
|
|
675
|
+
old: dict[str, str],
|
|
676
|
+
new: dict[str, str],
|
|
677
|
+
) -> list[DependencyResolution]:
|
|
678
|
+
"""Merge deps from three sources: new template + user-added (current - old)."""
|
|
679
|
+
resolutions: list[DependencyResolution] = []
|
|
680
|
+
user_added = set(current) - set(old)
|
|
681
|
+
gacli_managed = set(old)
|
|
682
|
+
|
|
683
|
+
for name, new_spec in new.items():
|
|
684
|
+
if name in old:
|
|
685
|
+
old_spec = old[name]
|
|
686
|
+
resolutions.append(
|
|
687
|
+
DependencyResolution(
|
|
688
|
+
name=name,
|
|
689
|
+
status="updated" if old_spec != new_spec else "unchanged",
|
|
690
|
+
old_version=old_spec,
|
|
691
|
+
new_version=new_spec,
|
|
692
|
+
)
|
|
693
|
+
)
|
|
694
|
+
elif name in user_added:
|
|
695
|
+
user_spec = current[name]
|
|
696
|
+
logging.warning(
|
|
697
|
+
f"Dependency '{name}' is now added by the template ({new_spec}) "
|
|
698
|
+
f"but your project already pins it ({user_spec}). Keeping your "
|
|
699
|
+
f"version; if the upgrade misbehaves, reconcile it with the "
|
|
700
|
+
f"template's expected {new_spec}."
|
|
701
|
+
)
|
|
702
|
+
else:
|
|
703
|
+
resolutions.append(
|
|
704
|
+
DependencyResolution(name=name, status="added", new_version=new_spec)
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
for name in user_added:
|
|
708
|
+
user_spec = current[name]
|
|
709
|
+
resolutions.append(
|
|
710
|
+
DependencyResolution(
|
|
711
|
+
name=name, status="kept", old_version=user_spec, new_version=user_spec
|
|
712
|
+
)
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
for name in gacli_managed:
|
|
716
|
+
if name not in new and name not in user_added:
|
|
717
|
+
resolutions.append(
|
|
718
|
+
DependencyResolution(name=name, status="removed", old_version=old[name])
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
return resolutions
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def merge_python_dependencies(
|
|
725
|
+
current_project: pathlib.Path,
|
|
726
|
+
old_template_project: pathlib.Path,
|
|
727
|
+
new_template_project: pathlib.Path,
|
|
728
|
+
) -> list[DependencyResolution]:
|
|
729
|
+
"""Merge Python deps: new_template + user_added, where user_added = current - old."""
|
|
730
|
+
return _resolve_dependencies(
|
|
731
|
+
_load_dependencies_from_pyproject(current_project / "pyproject.toml"),
|
|
732
|
+
_load_dependencies_from_pyproject(old_template_project / "pyproject.toml"),
|
|
733
|
+
_load_dependencies_from_pyproject(new_template_project / "pyproject.toml"),
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def write_python_dependencies(
|
|
738
|
+
project_dir: pathlib.Path,
|
|
739
|
+
resolutions: list[DependencyResolution],
|
|
740
|
+
) -> bool:
|
|
741
|
+
"""Write merged dependencies to project_dir/pyproject.toml using the uv CLI.
|
|
742
|
+
|
|
743
|
+
Uses ``uv add --frozen`` and ``uv remove --frozen`` so the lockfile and
|
|
744
|
+
virtualenv are left untouched; only pyproject.toml is modified.
|
|
745
|
+
"""
|
|
746
|
+
pyproject_path = project_dir / "pyproject.toml"
|
|
747
|
+
if not pyproject_path.exists():
|
|
748
|
+
return False
|
|
749
|
+
|
|
750
|
+
try:
|
|
751
|
+
from graph_agents_cli._runner import run_resolved
|
|
752
|
+
from graph_agents_cli._tools import ToolNotFoundError
|
|
753
|
+
|
|
754
|
+
ordered: list[DependencyResolution] = sorted(resolutions, key=lambda r: r.name)
|
|
755
|
+
to_remove = [r.name for r in ordered if r.status == "removed"]
|
|
756
|
+
to_add = [r.full_name() for r in ordered if r.status != "removed"]
|
|
757
|
+
|
|
758
|
+
if to_remove:
|
|
759
|
+
result = run_resolved(
|
|
760
|
+
["uv", "remove", "--frozen", *to_remove],
|
|
761
|
+
cwd=project_dir,
|
|
762
|
+
capture_output=True,
|
|
763
|
+
text=True,
|
|
764
|
+
)
|
|
765
|
+
if result.returncode != 0:
|
|
766
|
+
logging.warning(f"uv remove failed: {result.stderr}")
|
|
767
|
+
|
|
768
|
+
if to_add:
|
|
769
|
+
result = run_resolved(
|
|
770
|
+
["uv", "add", "--frozen", *to_add],
|
|
771
|
+
cwd=project_dir,
|
|
772
|
+
capture_output=True,
|
|
773
|
+
text=True,
|
|
774
|
+
)
|
|
775
|
+
if result.returncode != 0:
|
|
776
|
+
logging.warning(f"uv add failed: {result.stderr}")
|
|
777
|
+
return False
|
|
778
|
+
|
|
779
|
+
return True
|
|
780
|
+
except ToolNotFoundError:
|
|
781
|
+
logging.warning("uv not found; cannot write merged dependencies")
|
|
782
|
+
return False
|
|
783
|
+
except Exception as e:
|
|
784
|
+
logging.warning(f"Could not write dependencies to {pyproject_path}: {e}")
|
|
785
|
+
return False
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
# Per-language dependency handlers (Python only). None = not supported.
|
|
789
|
+
MERGE_DEPENDENCY_HANDLERS: dict[str, Callable | None] = {
|
|
790
|
+
"python": merge_python_dependencies,
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
WRITE_DEPENDENCY_HANDLERS: dict[str, Callable | None] = {
|
|
794
|
+
"python": write_python_dependencies,
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def update_cli_metadata(
|
|
799
|
+
project_dir: pathlib.Path,
|
|
800
|
+
create_params: dict[str, Any],
|
|
801
|
+
*,
|
|
802
|
+
cli_version: str | None = None,
|
|
803
|
+
remove_keys: list[str] | None = None,
|
|
804
|
+
base_template: str | None = None,
|
|
805
|
+
) -> None:
|
|
806
|
+
"""Update specific keys in the project manifest.
|
|
807
|
+
|
|
808
|
+
A manifest that cannot be read or written is logged and skipped: every
|
|
809
|
+
caller writes metadata as a side effect of work that already succeeded.
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
project_dir: Path to the project directory
|
|
813
|
+
create_params: Dict of keys to update inside create_params
|
|
814
|
+
cli_version: If provided, update cli_version
|
|
815
|
+
remove_keys: List of keys to remove from the create_params section
|
|
816
|
+
base_template: If provided, record it at the manifest top level
|
|
817
|
+
"""
|
|
818
|
+
manifest_path = project_dir / MANIFEST_FILENAME
|
|
819
|
+
if not manifest_path.exists():
|
|
820
|
+
logging.warning(f"Manifest not found: {manifest_path}")
|
|
821
|
+
return
|
|
822
|
+
|
|
823
|
+
try:
|
|
824
|
+
with open(manifest_path, encoding="utf-8") as f:
|
|
825
|
+
data = yaml.safe_load(f) or {}
|
|
826
|
+
|
|
827
|
+
if cli_version:
|
|
828
|
+
data["cli_version"] = cli_version
|
|
829
|
+
|
|
830
|
+
if base_template is not None:
|
|
831
|
+
data["base_template"] = base_template
|
|
832
|
+
|
|
833
|
+
if "create_params" not in data or not isinstance(data["create_params"], dict):
|
|
834
|
+
data["create_params"] = {}
|
|
835
|
+
|
|
836
|
+
params = data["create_params"]
|
|
837
|
+
|
|
838
|
+
for key, val in create_params.items():
|
|
839
|
+
params[key] = val
|
|
840
|
+
|
|
841
|
+
if remove_keys:
|
|
842
|
+
for key in remove_keys:
|
|
843
|
+
params.pop(key, None)
|
|
844
|
+
|
|
845
|
+
with open(manifest_path, "w", encoding="utf-8") as f:
|
|
846
|
+
yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False)
|
|
847
|
+
except Exception as e:
|
|
848
|
+
logging.warning(f"Could not update the manifest {manifest_path}: {e}")
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def compare_all_files(
|
|
852
|
+
project_dir: pathlib.Path,
|
|
853
|
+
old_template_dir: pathlib.Path,
|
|
854
|
+
new_template_dir: pathlib.Path,
|
|
855
|
+
agent_directory: str = "app",
|
|
856
|
+
*,
|
|
857
|
+
merge_config: bool = False,
|
|
858
|
+
) -> list[FileCompareResult]:
|
|
859
|
+
"""Compare all files using 3-way comparison (``merge_config``: see ``three_way_compare``)."""
|
|
860
|
+
all_files = collect_all_files(project_dir, old_template_dir, new_template_dir)
|
|
861
|
+
|
|
862
|
+
results = []
|
|
863
|
+
for relative_path in sorted(all_files):
|
|
864
|
+
result = three_way_compare(
|
|
865
|
+
relative_path,
|
|
866
|
+
project_dir,
|
|
867
|
+
old_template_dir,
|
|
868
|
+
new_template_dir,
|
|
869
|
+
agent_directory,
|
|
870
|
+
merge_config=merge_config,
|
|
871
|
+
)
|
|
872
|
+
results.append(result)
|
|
873
|
+
|
|
874
|
+
return results
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def group_results_by_action(
|
|
878
|
+
results: list[FileCompareResult],
|
|
879
|
+
) -> dict[str, list[FileCompareResult]]:
|
|
880
|
+
"""Group results by action type."""
|
|
881
|
+
groups: dict[str, list[FileCompareResult]] = {
|
|
882
|
+
"auto_update": [],
|
|
883
|
+
"preserve": [],
|
|
884
|
+
"skip": [],
|
|
885
|
+
"conflict": [],
|
|
886
|
+
"new": [],
|
|
887
|
+
"removed": [],
|
|
888
|
+
"merge": [],
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
for result in results:
|
|
892
|
+
groups[result.action].append(result)
|
|
893
|
+
|
|
894
|
+
return groups
|