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,970 @@
|
|
|
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
|
+
"""Shared merge utilities for upgrade and enhance commands.
|
|
17
|
+
|
|
18
|
+
Functions for generating template snapshots, displaying comparison results,
|
|
19
|
+
resolving conflicts, and applying file changes. ``run_three_way_merge`` is the
|
|
20
|
+
orchestrator shared by *upgrade* and *enhance*.
|
|
21
|
+
|
|
22
|
+
Baseline policy: the old snapshot is regenerated by the exact prior CLI
|
|
23
|
+
version (``uvx --from <pinned_install_spec(version)> graph-agents-cli``), or by
|
|
24
|
+
the exact build a caller names (``old_source``: ``scaffold upgrade
|
|
25
|
+
--baseline-ref``, or the commit a manifest records for a build between
|
|
26
|
+
releases), because the current templates would misclassify files that changed
|
|
27
|
+
between the two. When that build cannot be fetched and run, or
|
|
28
|
+
``GRAPH_AGENTS_CLI_INSTALL_SPEC`` is set without ``{version}`` (it would install
|
|
29
|
+
some other build), the merge stops with no changes unless ``baseline="current"``
|
|
30
|
+
was passed, an explicit and logged opt-in to compare against the current templates.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import difflib
|
|
34
|
+
import logging
|
|
35
|
+
import pathlib
|
|
36
|
+
import shutil
|
|
37
|
+
import tempfile
|
|
38
|
+
from collections.abc import Callable
|
|
39
|
+
from typing import Literal
|
|
40
|
+
|
|
41
|
+
import click
|
|
42
|
+
from rich.markup import escape
|
|
43
|
+
from rich.prompt import Prompt
|
|
44
|
+
|
|
45
|
+
from graph_agents_cli._output import Console
|
|
46
|
+
|
|
47
|
+
from .upgrade import (
|
|
48
|
+
MERGE_DEPENDENCY_HANDLERS,
|
|
49
|
+
WRITE_DEPENDENCY_HANDLERS,
|
|
50
|
+
DependencyReadError,
|
|
51
|
+
DependencyResolution,
|
|
52
|
+
FileCompareResult,
|
|
53
|
+
Followup,
|
|
54
|
+
compare_all_files,
|
|
55
|
+
group_results_by_action,
|
|
56
|
+
is_image_definition,
|
|
57
|
+
merged_config_text,
|
|
58
|
+
)
|
|
59
|
+
from .version import BaselineSource
|
|
60
|
+
|
|
61
|
+
console = Console()
|
|
62
|
+
|
|
63
|
+
# Maximum characters to display when showing diffs
|
|
64
|
+
MAX_DIFF_DISPLAY_CHARS = 2000
|
|
65
|
+
|
|
66
|
+
Baseline = Literal["authentic", "current"]
|
|
67
|
+
BASELINE_CURRENT_LABEL = "(baseline: current templates, --baseline current)"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def is_fetchable_version(version: str) -> bool:
|
|
71
|
+
"""True if ``version`` is worth attempting to fetch (a released version).
|
|
72
|
+
|
|
73
|
+
Rejects the unknown-version sentinel and unparseable strings; whether the
|
|
74
|
+
release actually exists is confirmed only by the fetch in ``_run_pinned_create``.
|
|
75
|
+
"""
|
|
76
|
+
from packaging import version as pkg_version
|
|
77
|
+
|
|
78
|
+
from .version import UNKNOWN_VERSION
|
|
79
|
+
|
|
80
|
+
if version == UNKNOWN_VERSION:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
pkg_version.parse(version)
|
|
85
|
+
except Exception:
|
|
86
|
+
return False
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _run_vendored_create(
|
|
91
|
+
args: list[str],
|
|
92
|
+
output_dir: pathlib.Path,
|
|
93
|
+
project_name: str,
|
|
94
|
+
) -> bool:
|
|
95
|
+
"""Generate the snapshot in-process with the bundled create command."""
|
|
96
|
+
from click import Context
|
|
97
|
+
|
|
98
|
+
from ..commands.create import create
|
|
99
|
+
|
|
100
|
+
cli_args = [project_name]
|
|
101
|
+
cli_args.extend(["--output-dir", str(output_dir)])
|
|
102
|
+
# This runs in-process, so its output lands in the user's terminal.
|
|
103
|
+
# --quiet keeps it silent. The pinned path deliberately does not pass it:
|
|
104
|
+
# older published releases might reject it, and uvx output is captured.
|
|
105
|
+
cli_args.extend(["--auto-approve", "--skip-deps", "--skip-checks", "--quiet"])
|
|
106
|
+
cli_args.extend(args)
|
|
107
|
+
|
|
108
|
+
logging.debug("Running vendored create command with args: %s", cli_args)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
ctx = Context(create, info_name="create")
|
|
112
|
+
create.parse_args(ctx, list(cli_args))
|
|
113
|
+
ctx.invoke(create, **ctx.params)
|
|
114
|
+
|
|
115
|
+
expected_dir = output_dir / project_name
|
|
116
|
+
if not expected_dir.exists():
|
|
117
|
+
logging.error(
|
|
118
|
+
f"Create command succeeded but project directory not found: {expected_dir}"
|
|
119
|
+
)
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
return True
|
|
123
|
+
except SystemExit as e:
|
|
124
|
+
if e.code == 0:
|
|
125
|
+
return True
|
|
126
|
+
logging.error(f"Create command failed with exit code {e.code}")
|
|
127
|
+
return False
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logging.error(f"Error running create command: {e}")
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _run_pinned_create(
|
|
134
|
+
args: list[str],
|
|
135
|
+
output_dir: pathlib.Path,
|
|
136
|
+
project_name: str,
|
|
137
|
+
version: str,
|
|
138
|
+
source: BaselineSource | None = None,
|
|
139
|
+
) -> bool:
|
|
140
|
+
"""Generate the snapshot via ``uvx --from <spec> graph-agents-cli scaffold create``.
|
|
141
|
+
|
|
142
|
+
The spec is ``source.spec`` when a build was named (``--baseline-ref``, or
|
|
143
|
+
the commit a manifest records), else ``pinned_install_spec(version)``.
|
|
144
|
+
Returns true if the snapshot was generated, false otherwise.
|
|
145
|
+
"""
|
|
146
|
+
from graph_agents_cli._runner import run_resolved
|
|
147
|
+
|
|
148
|
+
from .version import PACKAGE_NAME, pinned_install_spec
|
|
149
|
+
|
|
150
|
+
spec = source.spec if source is not None else pinned_install_spec(version)
|
|
151
|
+
if spec is None:
|
|
152
|
+
return False
|
|
153
|
+
refresh = ["--refresh-package", PACKAGE_NAME] if source is not None and source.refresh else []
|
|
154
|
+
cmd = [
|
|
155
|
+
"uvx",
|
|
156
|
+
*refresh,
|
|
157
|
+
"--from",
|
|
158
|
+
spec,
|
|
159
|
+
PACKAGE_NAME,
|
|
160
|
+
"scaffold",
|
|
161
|
+
"create",
|
|
162
|
+
project_name,
|
|
163
|
+
"--output-dir",
|
|
164
|
+
str(output_dir),
|
|
165
|
+
"--auto-approve",
|
|
166
|
+
"--skip-deps",
|
|
167
|
+
"--skip-checks",
|
|
168
|
+
*args,
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
logging.debug("Running pinned create via uvx: %s", cmd)
|
|
172
|
+
label = source.label if source is not None else version
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
result = run_resolved(cmd, capture_output=True, text=True)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logging.error("Could not run uvx to generate the %s template: %s", label, e)
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
if result.returncode != 0:
|
|
181
|
+
logging.debug("uvx create for %s exited %s:\n%s", label, result.returncode, result.stderr)
|
|
182
|
+
tail = [line for line in (result.stderr or "").splitlines() if line.strip()][-5:]
|
|
183
|
+
logging.error(
|
|
184
|
+
"Failed to generate the %s template via uvx%s",
|
|
185
|
+
label,
|
|
186
|
+
(":\n " + "\n ".join(tail)) if tail else ".",
|
|
187
|
+
)
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
expected_dir = output_dir / project_name
|
|
191
|
+
if not expected_dir.exists():
|
|
192
|
+
logging.error(
|
|
193
|
+
"uvx create for %s reported success but %s is missing.",
|
|
194
|
+
label,
|
|
195
|
+
expected_dir,
|
|
196
|
+
)
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def run_create_command(
|
|
203
|
+
args: list[str],
|
|
204
|
+
output_dir: pathlib.Path,
|
|
205
|
+
project_name: str,
|
|
206
|
+
version: str | None = None,
|
|
207
|
+
baseline: Baseline = "authentic",
|
|
208
|
+
source: BaselineSource | None = None,
|
|
209
|
+
) -> bool:
|
|
210
|
+
"""Generate a template snapshot for the 3-way upgrade/enhance merge.
|
|
211
|
+
|
|
212
|
+
A ``source`` (a named build: ``--baseline-ref``, or the commit a manifest
|
|
213
|
+
records) is rendered by that build through ``uvx``, whatever its version.
|
|
214
|
+
Otherwise no ``version``, or the running CLI's own version, renders with the
|
|
215
|
+
bundled templates, and a prior ``version`` is re-created by that exact
|
|
216
|
+
release through ``uvx`` (the authentic baseline). If the build cannot be
|
|
217
|
+
fetched and run (source unreachable, version absent, ``uvx`` missing,
|
|
218
|
+
unparseable version), the snapshot is NOT generated and False is returned,
|
|
219
|
+
so the caller stops with no changes. ``baseline="current"`` is the explicit,
|
|
220
|
+
logged opt-in to render the old snapshot with the current templates instead.
|
|
221
|
+
|
|
222
|
+
Returns True if the template was generated, False otherwise.
|
|
223
|
+
"""
|
|
224
|
+
from .version import (
|
|
225
|
+
PACKAGE_NAME,
|
|
226
|
+
get_current_version,
|
|
227
|
+
pinned_install_spec,
|
|
228
|
+
pinned_spec_unavailable,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if source is not None and baseline == "authentic":
|
|
232
|
+
if _run_pinned_create(args, output_dir, project_name, version or "", source):
|
|
233
|
+
return True
|
|
234
|
+
logging.error(
|
|
235
|
+
"Could not build the %s baseline from %s via uvx (source unreachable, ref "
|
|
236
|
+
"absent, or uvx missing). Nothing was changed.",
|
|
237
|
+
PACKAGE_NAME,
|
|
238
|
+
source.label,
|
|
239
|
+
)
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
if not version or version == get_current_version():
|
|
243
|
+
return _run_vendored_create(args, output_dir, project_name)
|
|
244
|
+
|
|
245
|
+
if baseline == "current":
|
|
246
|
+
logging.warning(
|
|
247
|
+
"Baseline override: rendering the %s snapshot with the CURRENT templates "
|
|
248
|
+
"(--baseline current). Files deleted or updated between %s and now may be "
|
|
249
|
+
"misclassified; review the result.",
|
|
250
|
+
version,
|
|
251
|
+
version,
|
|
252
|
+
)
|
|
253
|
+
console.print(
|
|
254
|
+
f"[yellow]Using the current templates as the {version} baseline "
|
|
255
|
+
"(--baseline current).[/yellow]"
|
|
256
|
+
)
|
|
257
|
+
return _run_vendored_create(args, output_dir, project_name)
|
|
258
|
+
|
|
259
|
+
if not is_fetchable_version(version):
|
|
260
|
+
logging.error(
|
|
261
|
+
"Cannot fetch %s@%s to build the authentic prior-version baseline: the "
|
|
262
|
+
"version is not a fetchable release. An inauthentic baseline would "
|
|
263
|
+
"misclassify files, so nothing was changed. Re-run with "
|
|
264
|
+
"`--baseline current` to compare against the current templates instead.",
|
|
265
|
+
PACKAGE_NAME,
|
|
266
|
+
version,
|
|
267
|
+
)
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
if pinned_install_spec(version) is None:
|
|
271
|
+
logging.error(
|
|
272
|
+
"Cannot build the authentic %s@%s baseline: %s. Nothing was changed; or re-run "
|
|
273
|
+
"with `--baseline current` to compare against the current templates instead.",
|
|
274
|
+
PACKAGE_NAME,
|
|
275
|
+
version,
|
|
276
|
+
pinned_spec_unavailable(version),
|
|
277
|
+
)
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
if _run_pinned_create(args, output_dir, project_name, version):
|
|
281
|
+
return True
|
|
282
|
+
|
|
283
|
+
logging.error(
|
|
284
|
+
"Could not build the %s@%s baseline via uvx (source unreachable, version "
|
|
285
|
+
"absent, or uvx missing). An inauthentic baseline would misclassify files, "
|
|
286
|
+
"so nothing was changed. Re-run with `--baseline current` to compare against "
|
|
287
|
+
"the current templates instead.",
|
|
288
|
+
PACKAGE_NAME,
|
|
289
|
+
version,
|
|
290
|
+
)
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def display_results(
|
|
295
|
+
groups: dict[str, list[FileCompareResult]],
|
|
296
|
+
dep_resolutions: list[DependencyResolution],
|
|
297
|
+
dry_run: bool = False,
|
|
298
|
+
baseline_current_of: str | None = None,
|
|
299
|
+
) -> None:
|
|
300
|
+
"""Display the comparison results grouped by action.
|
|
301
|
+
|
|
302
|
+
``baseline_current_of`` is the prior version when its snapshot was rendered
|
|
303
|
+
with the current templates (``--baseline current``): a file that differs
|
|
304
|
+
from them is then either the user's edit or one the templates changed
|
|
305
|
+
since that version, and the preserve list says so.
|
|
306
|
+
"""
|
|
307
|
+
if groups["auto_update"]:
|
|
308
|
+
console.print("[bold green]Will auto-update (unchanged by you):[/bold green]")
|
|
309
|
+
for result in groups["auto_update"]:
|
|
310
|
+
console.print(f" [green]✓[/green] {result.path}")
|
|
311
|
+
console.print()
|
|
312
|
+
|
|
313
|
+
preserved_user_modified = [
|
|
314
|
+
r for r in groups["preserve"] if r.preserve_type == "gacli_unchanged"
|
|
315
|
+
]
|
|
316
|
+
if preserved_user_modified:
|
|
317
|
+
if baseline_current_of:
|
|
318
|
+
console.print(
|
|
319
|
+
"[bold cyan]Will preserve (differs from the current template):[/bold cyan]\n"
|
|
320
|
+
f"[yellow] --baseline current cannot tell your edits from template changes "
|
|
321
|
+
f"since {baseline_current_of}: each file below that you did not edit keeps "
|
|
322
|
+
f"its {baseline_current_of} content, and dependency changes since then are "
|
|
323
|
+
"not merged. Prefer the authentic baseline.[/yellow]"
|
|
324
|
+
)
|
|
325
|
+
else:
|
|
326
|
+
console.print(
|
|
327
|
+
"[bold cyan]Will preserve (you modified, template unchanged):[/bold cyan]"
|
|
328
|
+
)
|
|
329
|
+
for result in preserved_user_modified:
|
|
330
|
+
console.print(f" [cyan]✓[/cyan] {result.path}")
|
|
331
|
+
console.print()
|
|
332
|
+
|
|
333
|
+
skipped = [r for r in groups["skip"] if r.category in ("agent_code", "config_files")]
|
|
334
|
+
if skipped:
|
|
335
|
+
console.print("[dim]Skipping (your code and config):[/dim]")
|
|
336
|
+
for result in skipped:
|
|
337
|
+
console.print(f" [dim]-[/dim] {result.path}")
|
|
338
|
+
console.print()
|
|
339
|
+
|
|
340
|
+
if groups.get("merge"):
|
|
341
|
+
console.print(
|
|
342
|
+
"[bold green]Will merge the template's change into your edited config:[/bold green]"
|
|
343
|
+
)
|
|
344
|
+
for result in groups["merge"]:
|
|
345
|
+
partly = " [dim](except keys you changed too)[/dim]" if result.followup else ""
|
|
346
|
+
console.print(f" [green]✓[/green] {result.path}{partly}")
|
|
347
|
+
console.print()
|
|
348
|
+
|
|
349
|
+
if groups["new"]:
|
|
350
|
+
console.print("[bold yellow]Files to add:[/bold yellow]")
|
|
351
|
+
for result in groups["new"]:
|
|
352
|
+
console.print(f" [yellow]+[/yellow] {result.path}")
|
|
353
|
+
console.print()
|
|
354
|
+
|
|
355
|
+
if groups["removed"]:
|
|
356
|
+
console.print("[bold yellow]Files to remove:[/bold yellow]")
|
|
357
|
+
for result in groups["removed"]:
|
|
358
|
+
console.print(f" [yellow]-[/yellow] {result.path}")
|
|
359
|
+
console.print()
|
|
360
|
+
|
|
361
|
+
if groups["conflict"]:
|
|
362
|
+
console.print("[bold red]Conflicts (both changed):[/bold red]")
|
|
363
|
+
for result in groups["conflict"]:
|
|
364
|
+
console.print(f" [red]⚠[/red] {result.path}")
|
|
365
|
+
if not dry_run:
|
|
366
|
+
console.print("[dim] You'll be prompted to resolve each conflict.[/dim]")
|
|
367
|
+
console.print()
|
|
368
|
+
|
|
369
|
+
# Only show deps that actually changed, not unchanged ones.
|
|
370
|
+
changed = [r for r in dep_resolutions if r.status != "unchanged"]
|
|
371
|
+
if changed:
|
|
372
|
+
console.print("[bold]Dependency changes:[/bold]")
|
|
373
|
+
for resolution in changed:
|
|
374
|
+
dep_name = escape(resolution.name)
|
|
375
|
+
old_ver = escape(resolution.old_version or "")
|
|
376
|
+
new_ver = escape(resolution.new_version or "")
|
|
377
|
+
if resolution.status == "updated":
|
|
378
|
+
console.print(f" [green]✓[/green] Update: {dep_name} {old_ver} → {new_ver}")
|
|
379
|
+
elif resolution.status == "added":
|
|
380
|
+
console.print(f" [green]+[/green] Add: {dep_name}{new_ver}")
|
|
381
|
+
elif resolution.status == "kept":
|
|
382
|
+
console.print(f" [cyan]✓[/cyan] Keep (yours): {dep_name}{old_ver}")
|
|
383
|
+
elif resolution.status == "removed":
|
|
384
|
+
console.print(f" [yellow]-[/yellow] Remove: {dep_name}{old_ver}")
|
|
385
|
+
console.print()
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def handle_conflict(
|
|
389
|
+
*,
|
|
390
|
+
result: FileCompareResult,
|
|
391
|
+
project_dir: pathlib.Path,
|
|
392
|
+
new_template_dir: pathlib.Path,
|
|
393
|
+
auto_approve: bool,
|
|
394
|
+
prefer_new: bool = False,
|
|
395
|
+
interactive: bool = False,
|
|
396
|
+
) -> str:
|
|
397
|
+
"""Handle a file conflict.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Action taken: "kept", "kept_all", "updated", "updated_all", or "skipped"
|
|
401
|
+
"""
|
|
402
|
+
if not interactive:
|
|
403
|
+
# Non-interactive (auto-approve or strict programmatic): keep the user's version
|
|
404
|
+
if prefer_new:
|
|
405
|
+
console.print(f" [green]Using new version: {result.path}[/green]")
|
|
406
|
+
return "updated"
|
|
407
|
+
logging.warning(
|
|
408
|
+
"Conflict: %s: keeping your version (use --prefer-new to override)",
|
|
409
|
+
result.path,
|
|
410
|
+
)
|
|
411
|
+
return "kept"
|
|
412
|
+
|
|
413
|
+
console.print(f"\n[bold yellow]Conflict: {result.path}[/bold yellow]")
|
|
414
|
+
console.print(f" Reason: {result.reason}")
|
|
415
|
+
|
|
416
|
+
choice = Prompt.ask(
|
|
417
|
+
" (v)iew diff, (k)eep yours, (K)eep all, (u)se new, (U)se all, (s)kip",
|
|
418
|
+
choices=["v", "k", "K", "u", "U", "s"],
|
|
419
|
+
default="k",
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
if choice == "v":
|
|
423
|
+
current_file = project_dir / result.path
|
|
424
|
+
new_file = new_template_dir / result.path
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
current_lines = current_file.read_text(encoding="utf-8").splitlines(keepends=True)
|
|
428
|
+
new_lines = new_file.read_text(encoding="utf-8").splitlines(keepends=True)
|
|
429
|
+
|
|
430
|
+
diff_lines = list(
|
|
431
|
+
difflib.unified_diff(
|
|
432
|
+
current_lines,
|
|
433
|
+
new_lines,
|
|
434
|
+
fromfile=f"Your version: {result.path}",
|
|
435
|
+
tofile=f"New version: {result.path}",
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
diff_output = "".join(diff_lines)
|
|
439
|
+
|
|
440
|
+
console.print()
|
|
441
|
+
if diff_output:
|
|
442
|
+
if len(diff_output) > MAX_DIFF_DISPLAY_CHARS:
|
|
443
|
+
console.print(diff_output[:MAX_DIFF_DISPLAY_CHARS])
|
|
444
|
+
console.print("[dim]... (truncated)[/dim]")
|
|
445
|
+
else:
|
|
446
|
+
console.print(diff_output)
|
|
447
|
+
else:
|
|
448
|
+
console.print("[dim]No differences found[/dim]")
|
|
449
|
+
except Exception as e:
|
|
450
|
+
console.print(f"[red]Could not show diff: {e}[/red]")
|
|
451
|
+
|
|
452
|
+
choice = Prompt.ask(
|
|
453
|
+
" (k)eep yours, (K)eep all, (u)se new, (U)se all, (s)kip",
|
|
454
|
+
choices=["k", "K", "u", "U", "s"],
|
|
455
|
+
default="k",
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
if choice == "K":
|
|
459
|
+
console.print(" [cyan]Keeping your version for all conflicts[/cyan]")
|
|
460
|
+
return "kept_all"
|
|
461
|
+
elif choice == "U":
|
|
462
|
+
console.print(" [green]Using new version for all conflicts[/green]")
|
|
463
|
+
return "updated_all"
|
|
464
|
+
elif choice == "k":
|
|
465
|
+
console.print(" [cyan]Keeping your version[/cyan]")
|
|
466
|
+
return "kept"
|
|
467
|
+
elif choice == "u":
|
|
468
|
+
return "updated"
|
|
469
|
+
else:
|
|
470
|
+
return "skipped"
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def copy_file(src: pathlib.Path, dst: pathlib.Path) -> bool:
|
|
474
|
+
"""Copy a file, creating parent directories as needed."""
|
|
475
|
+
if not src.exists():
|
|
476
|
+
return False
|
|
477
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
478
|
+
shutil.copy2(src, dst)
|
|
479
|
+
return True
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def apply_changes(
|
|
483
|
+
*,
|
|
484
|
+
groups: dict[str, list[FileCompareResult]],
|
|
485
|
+
project_dir: pathlib.Path,
|
|
486
|
+
new_template_dir: pathlib.Path,
|
|
487
|
+
auto_approve: bool,
|
|
488
|
+
dry_run: bool,
|
|
489
|
+
prefer_new: bool = False,
|
|
490
|
+
interactive: bool = False,
|
|
491
|
+
old_template_dir: pathlib.Path | None = None,
|
|
492
|
+
) -> dict[str, int]:
|
|
493
|
+
"""Apply file changes to the project (``merge`` results need ``old_template_dir``)."""
|
|
494
|
+
counts = {
|
|
495
|
+
"updated": 0,
|
|
496
|
+
"added": 0,
|
|
497
|
+
"removed": 0,
|
|
498
|
+
"skipped": 0,
|
|
499
|
+
"conflicts_kept": 0,
|
|
500
|
+
"conflicts_updated": 0,
|
|
501
|
+
"merged": 0,
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if dry_run:
|
|
505
|
+
console.print("[bold yellow]Dry run - no changes made[/bold yellow]")
|
|
506
|
+
return counts
|
|
507
|
+
|
|
508
|
+
for result in groups["auto_update"]:
|
|
509
|
+
if copy_file(new_template_dir / result.path, project_dir / result.path):
|
|
510
|
+
counts["updated"] += 1
|
|
511
|
+
|
|
512
|
+
for result in groups.get("merge", []):
|
|
513
|
+
merged = (
|
|
514
|
+
merged_config_text(project_dir, old_template_dir, new_template_dir, result.path)
|
|
515
|
+
if old_template_dir is not None
|
|
516
|
+
else None
|
|
517
|
+
)
|
|
518
|
+
if merged is None:
|
|
519
|
+
# Classified as mergeable a moment ago; never guess if that changed.
|
|
520
|
+
counts["skipped"] += 1
|
|
521
|
+
continue
|
|
522
|
+
(project_dir / result.path).write_text(merged, encoding="utf-8")
|
|
523
|
+
counts["merged"] += 1
|
|
524
|
+
|
|
525
|
+
for result in groups["new"]:
|
|
526
|
+
if copy_file(new_template_dir / result.path, project_dir / result.path):
|
|
527
|
+
counts["added"] += 1
|
|
528
|
+
|
|
529
|
+
for result in groups["removed"]:
|
|
530
|
+
file_path = project_dir / result.path
|
|
531
|
+
if file_path.exists():
|
|
532
|
+
file_path.unlink()
|
|
533
|
+
counts["removed"] += 1
|
|
534
|
+
|
|
535
|
+
if groups["conflict"]:
|
|
536
|
+
console.print()
|
|
537
|
+
console.print("[bold]Resolving conflicts:[/bold]")
|
|
538
|
+
|
|
539
|
+
bulk_action = None # "keep" or "update" when user chooses K or U
|
|
540
|
+
for result in groups["conflict"]:
|
|
541
|
+
if bulk_action == "keep":
|
|
542
|
+
console.print(f" [dim]Keeping your version: {result.path}[/dim]")
|
|
543
|
+
counts["conflicts_kept"] += 1
|
|
544
|
+
continue
|
|
545
|
+
elif bulk_action == "update":
|
|
546
|
+
if copy_file(new_template_dir / result.path, project_dir / result.path):
|
|
547
|
+
console.print(f" [green]Updated: {result.path}[/green]")
|
|
548
|
+
counts["conflicts_updated"] += 1
|
|
549
|
+
continue
|
|
550
|
+
|
|
551
|
+
action = handle_conflict(
|
|
552
|
+
result=result,
|
|
553
|
+
project_dir=project_dir,
|
|
554
|
+
new_template_dir=new_template_dir,
|
|
555
|
+
auto_approve=auto_approve,
|
|
556
|
+
prefer_new=prefer_new,
|
|
557
|
+
interactive=interactive,
|
|
558
|
+
)
|
|
559
|
+
if action == "kept_all":
|
|
560
|
+
counts["conflicts_kept"] += 1
|
|
561
|
+
bulk_action = "keep"
|
|
562
|
+
elif action == "updated_all":
|
|
563
|
+
if copy_file(new_template_dir / result.path, project_dir / result.path):
|
|
564
|
+
counts["conflicts_updated"] += 1
|
|
565
|
+
bulk_action = "update"
|
|
566
|
+
elif action == "updated":
|
|
567
|
+
if copy_file(new_template_dir / result.path, project_dir / result.path):
|
|
568
|
+
counts["conflicts_updated"] += 1
|
|
569
|
+
elif action == "kept":
|
|
570
|
+
counts["conflicts_kept"] += 1
|
|
571
|
+
else:
|
|
572
|
+
counts["skipped"] += 1
|
|
573
|
+
|
|
574
|
+
return counts
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class IncompleteMergeError(click.ClickException):
|
|
578
|
+
"""The merge was applied, but items marked required are left for the developer (exit 1).
|
|
579
|
+
|
|
580
|
+
Until they are done the project does not build or deploy with the settings
|
|
581
|
+
its manifest records, so a script must not carry on as if it did.
|
|
582
|
+
"""
|
|
583
|
+
|
|
584
|
+
exit_code = 1
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def print_followups(followups: list[str | Followup], *, dry_run: bool = False) -> None:
|
|
588
|
+
"""Print what the developer still has to do by hand, one numbered item each."""
|
|
589
|
+
if not followups:
|
|
590
|
+
return
|
|
591
|
+
console.print()
|
|
592
|
+
title = "Left for you after applying" if dry_run else "Left for you"
|
|
593
|
+
console.print(f"[bold yellow]{title}:[/bold yellow]")
|
|
594
|
+
for number, item in enumerate(followups, 1):
|
|
595
|
+
text = item.text if isinstance(item, Followup) else item
|
|
596
|
+
required = isinstance(item, Followup) and item.required
|
|
597
|
+
first, _, rest = text.partition("\n")
|
|
598
|
+
mark = "[bold red](required)[/bold red] " if required else ""
|
|
599
|
+
console.print(f" {number}. {mark}{escape(first)}")
|
|
600
|
+
if rest:
|
|
601
|
+
console.print(rest.rstrip("\n"), highlight=False, markup=False)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _finish(followups: list[str | Followup], *, dry_run: bool) -> None:
|
|
605
|
+
"""Print the follow-ups; raise :class:`IncompleteMergeError` when a required one is left."""
|
|
606
|
+
print_followups(followups, dry_run=dry_run)
|
|
607
|
+
required = [f for f in followups if isinstance(f, Followup) and f.required]
|
|
608
|
+
if required and not dry_run:
|
|
609
|
+
raise IncompleteMergeError(
|
|
610
|
+
f"{len(required)} item(s) marked (required) above must be done by hand: until then "
|
|
611
|
+
"the project does not build or deploy with the settings "
|
|
612
|
+
"graph-agents-cli-manifest.yaml records."
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _same_file(a: pathlib.Path, b: pathlib.Path) -> bool:
|
|
617
|
+
try:
|
|
618
|
+
return a.read_bytes() == b.read_bytes()
|
|
619
|
+
except OSError:
|
|
620
|
+
return False
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _sidecar_path(project_dir: pathlib.Path, relative_path: str, source: pathlib.Path) -> str:
|
|
624
|
+
"""``<file>.new`` (or ``.new.2``, ...): a free name, or one already holding ``source``."""
|
|
625
|
+
candidate = f"{relative_path}.new"
|
|
626
|
+
number = 1
|
|
627
|
+
while (project_dir / candidate).exists() and not _same_file(project_dir / candidate, source):
|
|
628
|
+
number += 1
|
|
629
|
+
candidate = f"{relative_path}.new.{number}"
|
|
630
|
+
return candidate
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _kept_conflict_followups(
|
|
634
|
+
conflicts: list[FileCompareResult],
|
|
635
|
+
*,
|
|
636
|
+
project_dir: pathlib.Path,
|
|
637
|
+
old_template_dir: pathlib.Path,
|
|
638
|
+
new_template_dir: pathlib.Path,
|
|
639
|
+
dry_run: bool,
|
|
640
|
+
prefer_new: bool,
|
|
641
|
+
) -> list[Followup]:
|
|
642
|
+
"""What is left for each conflict that kept the developer's version (enhance).
|
|
643
|
+
|
|
644
|
+
Every difference between the two snapshots of an enhance comes from the
|
|
645
|
+
settings change, so a kept conflict is part of that change that did not
|
|
646
|
+
happen: its template diff is listed. A file that defines the image cannot be
|
|
647
|
+
left behind (it would build the old settings): the template's version is
|
|
648
|
+
written next to it as ``<file>.new`` and the item is required.
|
|
649
|
+
"""
|
|
650
|
+
from .merge3 import template_diff
|
|
651
|
+
|
|
652
|
+
items: list[Followup] = []
|
|
653
|
+
for result in conflicts:
|
|
654
|
+
current = project_dir / result.path
|
|
655
|
+
new_file = new_template_dir / result.path
|
|
656
|
+
if dry_run and prefer_new:
|
|
657
|
+
continue
|
|
658
|
+
if not dry_run and new_file.exists() and _same_file(current, new_file):
|
|
659
|
+
continue # resolved in favour of the new version
|
|
660
|
+
if not new_file.exists():
|
|
661
|
+
items.append(
|
|
662
|
+
Followup(
|
|
663
|
+
f"{result.path}: you edited it and the new settings no longer use it; it "
|
|
664
|
+
"was kept, delete it if nothing needs it"
|
|
665
|
+
)
|
|
666
|
+
)
|
|
667
|
+
continue
|
|
668
|
+
if is_image_definition(result.path):
|
|
669
|
+
sidecar = _sidecar_path(project_dir, result.path, new_file)
|
|
670
|
+
if not dry_run:
|
|
671
|
+
copy_file(new_file, project_dir / sidecar)
|
|
672
|
+
verb = "will be" if dry_run else "was"
|
|
673
|
+
items.append(
|
|
674
|
+
Followup(
|
|
675
|
+
f"{result.path}: your edited version was kept, and it builds the image for "
|
|
676
|
+
f"the old settings. The template's {result.path} for the new settings {verb} "
|
|
677
|
+
f"written to {sidecar}: carry your edits over to it, then replace "
|
|
678
|
+
f"{result.path} with it (and delete {sidecar}).",
|
|
679
|
+
required=True,
|
|
680
|
+
)
|
|
681
|
+
)
|
|
682
|
+
continue
|
|
683
|
+
old_file = old_template_dir / result.path
|
|
684
|
+
old_text = (
|
|
685
|
+
old_file.read_text(encoding="utf-8", errors="replace") if old_file.exists() else ""
|
|
686
|
+
)
|
|
687
|
+
new_text = new_file.read_text(encoding="utf-8", errors="replace")
|
|
688
|
+
items.append(
|
|
689
|
+
Followup(
|
|
690
|
+
f"{result.path}: you edited it and the new settings change it too; your version "
|
|
691
|
+
"was kept. Apply the template's change by hand:\n"
|
|
692
|
+
+ template_diff(old_text, new_text, result.path)
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
return items
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def run_three_way_merge(
|
|
699
|
+
*,
|
|
700
|
+
project_dir: pathlib.Path,
|
|
701
|
+
project_name: str,
|
|
702
|
+
agent_directory: str,
|
|
703
|
+
language: str,
|
|
704
|
+
old_args: list[str],
|
|
705
|
+
new_args: list[str],
|
|
706
|
+
old_version: str | None = None,
|
|
707
|
+
baseline: Baseline = "authentic",
|
|
708
|
+
old_source: BaselineSource | None = None,
|
|
709
|
+
auto_approve: bool,
|
|
710
|
+
dry_run: bool,
|
|
711
|
+
prefer_new: bool = False,
|
|
712
|
+
interactive: bool = False,
|
|
713
|
+
operation_label: str = "upgrade",
|
|
714
|
+
pre_apply_hook: Callable[[pathlib.Path], bool] | None = None,
|
|
715
|
+
post_apply_hook: Callable[[pathlib.Path, str], list[str | Followup] | None] | None = None,
|
|
716
|
+
merge_config: bool = False,
|
|
717
|
+
snapshot_check: Callable[[pathlib.Path, pathlib.Path], None] | None = None,
|
|
718
|
+
) -> bool:
|
|
719
|
+
"""Shared 3-way merge pipeline used by both *upgrade* and *enhance*.
|
|
720
|
+
|
|
721
|
+
Generates old and new template snapshots, compares every file against
|
|
722
|
+
the current project, displays the diff summary, and applies changes.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
project_dir: Resolved path to the user's project.
|
|
726
|
+
project_name: Name used when generating the template.
|
|
727
|
+
agent_directory: Subdirectory containing agent code (e.g. "app").
|
|
728
|
+
language: Project language ("python").
|
|
729
|
+
old_args: CLI args for re-creating the *old* template snapshot.
|
|
730
|
+
new_args: CLI args for re-creating the *new* template snapshot.
|
|
731
|
+
old_version: If set, the prior CLI version that renders the old snapshot.
|
|
732
|
+
baseline: "authentic" (default) stops when ``old_version`` cannot be
|
|
733
|
+
fetched; "current" renders the old snapshot with the current templates.
|
|
734
|
+
old_source: The build that renders the old snapshot, when one is named
|
|
735
|
+
(it wins over ``old_version``'s release; authentic baseline only).
|
|
736
|
+
auto_approve: Auto-apply non-conflicting changes without prompts.
|
|
737
|
+
dry_run: Preview changes without writing anything.
|
|
738
|
+
prefer_new: Resolve conflicts in favour of the new template.
|
|
739
|
+
interactive: Allow interactive conflict-resolution prompts.
|
|
740
|
+
operation_label: Human-readable verb for prompt/log text.
|
|
741
|
+
pre_apply_hook: Callback invoked before files are written; return False to abort.
|
|
742
|
+
post_apply_hook: Callback invoked after files are written (and deps merged);
|
|
743
|
+
it may return follow-up items for the developer, printed at the end.
|
|
744
|
+
merge_config: Merge the config files the new settings re-render
|
|
745
|
+
(``upgrade.STRUCTURAL_CONFIG_FILES``) instead of skipping them, and
|
|
746
|
+
list what every kept conflict leaves undone (enhance).
|
|
747
|
+
snapshot_check: Called with the old and new snapshot directories once
|
|
748
|
+
both exist, before anything is compared; it may raise a
|
|
749
|
+
``click.ClickException`` to stop with the project unchanged.
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
True if the pipeline completed (changes applied, user cancelled, or
|
|
753
|
+
nothing to do). False if a template snapshot could not be generated;
|
|
754
|
+
the project was not modified.
|
|
755
|
+
|
|
756
|
+
Raises:
|
|
757
|
+
IncompleteMergeError: changes were applied, but a follow-up marked
|
|
758
|
+
required is left (an image definition that kept the old settings,
|
|
759
|
+
dependency changes that could not be written, or whatever the
|
|
760
|
+
post-apply hook marks required). Never raised under ``dry_run``.
|
|
761
|
+
"""
|
|
762
|
+
same_config = (
|
|
763
|
+
sorted(old_args) == sorted(new_args) and old_version is None and old_source is None
|
|
764
|
+
)
|
|
765
|
+
baseline_label = f" {BASELINE_CURRENT_LABEL}" if baseline == "current" and old_version else ""
|
|
766
|
+
|
|
767
|
+
temp_base = pathlib.Path(tempfile.mkdtemp(prefix=f"gacli_{operation_label}_"))
|
|
768
|
+
old_template_dir = temp_base / "old"
|
|
769
|
+
new_template_dir = temp_base / "new"
|
|
770
|
+
|
|
771
|
+
try:
|
|
772
|
+
console.print()
|
|
773
|
+
console.print("[dim]Generating templates for comparison...[/dim]")
|
|
774
|
+
|
|
775
|
+
if same_config:
|
|
776
|
+
# Identical args: generate once, reuse for both.
|
|
777
|
+
console.print("[dim] - Template...[/dim]")
|
|
778
|
+
if not run_create_command(old_args, old_template_dir, project_name):
|
|
779
|
+
console.print("[bold red]Error:[/bold red] Failed to generate template")
|
|
780
|
+
return False
|
|
781
|
+
old_template_project = old_template_dir / project_name
|
|
782
|
+
new_template_project = old_template_project
|
|
783
|
+
else:
|
|
784
|
+
console.print("[dim] - Old template...[/dim]")
|
|
785
|
+
if not run_create_command(
|
|
786
|
+
old_args, old_template_dir, project_name, old_version, baseline, old_source
|
|
787
|
+
):
|
|
788
|
+
console.print(
|
|
789
|
+
"[bold red]Error:[/bold red] Failed to generate the old template snapshot"
|
|
790
|
+
)
|
|
791
|
+
return False
|
|
792
|
+
|
|
793
|
+
console.print("[dim] - New template...[/dim]")
|
|
794
|
+
if not run_create_command(new_args, new_template_dir, project_name):
|
|
795
|
+
console.print("[bold red]Error:[/bold red] Failed to generate new template")
|
|
796
|
+
return False
|
|
797
|
+
|
|
798
|
+
old_template_project = old_template_dir / project_name
|
|
799
|
+
new_template_project = new_template_dir / project_name
|
|
800
|
+
|
|
801
|
+
if snapshot_check is not None:
|
|
802
|
+
snapshot_check(old_template_project, new_template_project)
|
|
803
|
+
|
|
804
|
+
console.print()
|
|
805
|
+
|
|
806
|
+
console.print("[dim]Comparing files...[/dim]")
|
|
807
|
+
results = compare_all_files(
|
|
808
|
+
project_dir,
|
|
809
|
+
old_template_project,
|
|
810
|
+
new_template_project,
|
|
811
|
+
agent_directory,
|
|
812
|
+
merge_config=merge_config,
|
|
813
|
+
)
|
|
814
|
+
groups = group_results_by_action(results)
|
|
815
|
+
followups: list[str | Followup] = [r.followup for r in results if r.followup]
|
|
816
|
+
|
|
817
|
+
dep_resolutions: list[DependencyResolution] = []
|
|
818
|
+
merge_dependencies = MERGE_DEPENDENCY_HANDLERS.get(language)
|
|
819
|
+
if merge_dependencies is None:
|
|
820
|
+
logging.warning(
|
|
821
|
+
"Dependency merging isn't supported for %s projects; your "
|
|
822
|
+
"dependency manifest was left unchanged. Review it manually "
|
|
823
|
+
"after the %s.",
|
|
824
|
+
language,
|
|
825
|
+
operation_label,
|
|
826
|
+
)
|
|
827
|
+
else:
|
|
828
|
+
try:
|
|
829
|
+
dep_resolutions = merge_dependencies(
|
|
830
|
+
project_dir, old_template_project, new_template_project
|
|
831
|
+
)
|
|
832
|
+
except DependencyReadError as e:
|
|
833
|
+
logging.warning(
|
|
834
|
+
"Skipping dependency merge: %s. Review your dependency manifest "
|
|
835
|
+
"manually after the %s.",
|
|
836
|
+
e,
|
|
837
|
+
operation_label,
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
console.print()
|
|
841
|
+
|
|
842
|
+
display_results(
|
|
843
|
+
groups,
|
|
844
|
+
dep_resolutions,
|
|
845
|
+
dry_run,
|
|
846
|
+
baseline_current_of=old_version if baseline == "current" else None,
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
total_changes = (
|
|
850
|
+
len(groups["auto_update"])
|
|
851
|
+
+ len(groups["new"])
|
|
852
|
+
+ len(groups["removed"])
|
|
853
|
+
+ len(groups["conflict"])
|
|
854
|
+
+ len(groups["merge"])
|
|
855
|
+
)
|
|
856
|
+
has_dep_changes = any(r.status != "unchanged" for r in dep_resolutions)
|
|
857
|
+
if total_changes == 0 and not has_dep_changes:
|
|
858
|
+
if post_apply_hook and not dry_run:
|
|
859
|
+
followups.extend(post_apply_hook(project_dir, language) or [])
|
|
860
|
+
console.print(f"[bold green]✅[/bold green] No changes needed!{baseline_label}")
|
|
861
|
+
_finish(followups, dry_run=dry_run)
|
|
862
|
+
return True
|
|
863
|
+
|
|
864
|
+
if interactive and not dry_run:
|
|
865
|
+
prompt_text = f"\nProceed with {operation_label}?"
|
|
866
|
+
if groups["conflict"]:
|
|
867
|
+
prompt_text = "\nProceed? (you'll resolve conflicts next)"
|
|
868
|
+
proceed = Prompt.ask(
|
|
869
|
+
prompt_text,
|
|
870
|
+
choices=["y", "n"],
|
|
871
|
+
case_sensitive=False,
|
|
872
|
+
default="y",
|
|
873
|
+
).lower()
|
|
874
|
+
if proceed != "y":
|
|
875
|
+
console.print(f"[yellow]{operation_label.capitalize()} cancelled.[/yellow]")
|
|
876
|
+
return True
|
|
877
|
+
|
|
878
|
+
if pre_apply_hook and not dry_run:
|
|
879
|
+
if not pre_apply_hook(project_dir):
|
|
880
|
+
return True # hook signalled abort (e.g. user cancelled backup)
|
|
881
|
+
|
|
882
|
+
counts = apply_changes(
|
|
883
|
+
groups=groups,
|
|
884
|
+
project_dir=project_dir,
|
|
885
|
+
new_template_dir=new_template_project,
|
|
886
|
+
auto_approve=auto_approve,
|
|
887
|
+
dry_run=dry_run,
|
|
888
|
+
prefer_new=prefer_new,
|
|
889
|
+
interactive=interactive,
|
|
890
|
+
old_template_dir=old_template_project,
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
if merge_config:
|
|
894
|
+
followups.extend(
|
|
895
|
+
_kept_conflict_followups(
|
|
896
|
+
groups["conflict"],
|
|
897
|
+
project_dir=project_dir,
|
|
898
|
+
old_template_dir=old_template_project,
|
|
899
|
+
new_template_dir=new_template_project,
|
|
900
|
+
dry_run=dry_run,
|
|
901
|
+
prefer_new=prefer_new,
|
|
902
|
+
)
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
write_dependencies = WRITE_DEPENDENCY_HANDLERS.get(language)
|
|
906
|
+
if not dry_run and has_dep_changes and write_dependencies is not None:
|
|
907
|
+
if write_dependencies(project_dir, dep_resolutions):
|
|
908
|
+
console.print(
|
|
909
|
+
"[dim] Dependencies updated; run [bold]graph-agents-cli install[/bold] "
|
|
910
|
+
"to install the new versions.[/dim]"
|
|
911
|
+
)
|
|
912
|
+
if merge_config:
|
|
913
|
+
followups.append(
|
|
914
|
+
Followup(
|
|
915
|
+
"Run `graph-agents-cli install` to bring uv.lock (and .venv) up to "
|
|
916
|
+
"date with the new dependencies: the image installs exactly what "
|
|
917
|
+
"uv.lock pins"
|
|
918
|
+
)
|
|
919
|
+
)
|
|
920
|
+
elif merge_config:
|
|
921
|
+
changes = ", ".join(
|
|
922
|
+
f"-{r.name}" if r.status == "removed" else f"+{r.full_name()}"
|
|
923
|
+
for r in dep_resolutions
|
|
924
|
+
if r.status in ("added", "updated", "removed")
|
|
925
|
+
)
|
|
926
|
+
followups.append(
|
|
927
|
+
Followup(
|
|
928
|
+
"pyproject.toml: the dependency changes for the new settings could not "
|
|
929
|
+
f"be written (see the warning above); make them by hand: {changes}",
|
|
930
|
+
required=True,
|
|
931
|
+
)
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
if post_apply_hook and not dry_run:
|
|
935
|
+
followups.extend(post_apply_hook(project_dir, language) or [])
|
|
936
|
+
|
|
937
|
+
console.print()
|
|
938
|
+
if dry_run:
|
|
939
|
+
console.print(
|
|
940
|
+
"[bold yellow]Dry run complete.[/bold yellow] "
|
|
941
|
+
f"Run without --dry-run to apply changes.{baseline_label}"
|
|
942
|
+
)
|
|
943
|
+
else:
|
|
944
|
+
console.print(f" Updated: {counts['updated']} files")
|
|
945
|
+
if counts["merged"]:
|
|
946
|
+
console.print(f" Merged into your edits: {counts['merged']} files")
|
|
947
|
+
console.print(f" Added: {counts['added']} files")
|
|
948
|
+
console.print(f" Removed: {counts['removed']} files")
|
|
949
|
+
if counts["conflicts_kept"] or counts["conflicts_updated"]:
|
|
950
|
+
console.print(
|
|
951
|
+
f" Conflicts: {counts['conflicts_updated']} updated, "
|
|
952
|
+
f"{counts['conflicts_kept']} kept yours"
|
|
953
|
+
)
|
|
954
|
+
console.print()
|
|
955
|
+
if any(isinstance(f, Followup) and f.required for f in followups):
|
|
956
|
+
console.print(
|
|
957
|
+
f"[bold yellow]⚠️ {operation_label.capitalize()} applied, with required "
|
|
958
|
+
f"steps left for you (below).[/bold yellow]{baseline_label}"
|
|
959
|
+
)
|
|
960
|
+
else:
|
|
961
|
+
console.print(
|
|
962
|
+
f"[bold green]✅ {operation_label.capitalize()} complete![/bold green]"
|
|
963
|
+
f"{baseline_label}"
|
|
964
|
+
)
|
|
965
|
+
_finish(followups, dry_run=dry_run)
|
|
966
|
+
|
|
967
|
+
return True
|
|
968
|
+
|
|
969
|
+
finally:
|
|
970
|
+
shutil.rmtree(temp_base, ignore_errors=True)
|