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,1652 @@
|
|
|
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
|
+
"""``scaffold enhance``: add or change the deployment target, CD mode, or runtime."""
|
|
17
|
+
|
|
18
|
+
import dataclasses
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import pathlib
|
|
22
|
+
import shlex
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
import click
|
|
28
|
+
from packaging import version as pkg_version
|
|
29
|
+
from rich.markup import escape
|
|
30
|
+
from rich.prompt import IntPrompt, Prompt
|
|
31
|
+
|
|
32
|
+
from graph_agents_cli import _api_policy
|
|
33
|
+
from graph_agents_cli._defaults import (
|
|
34
|
+
DEFAULT_AGENT_GUIDANCE_FILENAME,
|
|
35
|
+
DEFAULT_MODELS,
|
|
36
|
+
DEFAULT_REGISTRY_HOST,
|
|
37
|
+
DEFAULT_REGISTRY_PLACEHOLDER,
|
|
38
|
+
PROVIDER_KEY_VARS,
|
|
39
|
+
default_secret_keys,
|
|
40
|
+
normalize_auth_policy,
|
|
41
|
+
)
|
|
42
|
+
from graph_agents_cli._output import Console
|
|
43
|
+
from graph_agents_cli._project import (
|
|
44
|
+
API_POLICY_FILENAME,
|
|
45
|
+
NotInProjectError,
|
|
46
|
+
ProjectConfig,
|
|
47
|
+
find_project_config,
|
|
48
|
+
find_project_root,
|
|
49
|
+
)
|
|
50
|
+
from graph_agents_cli._runner import run_resolved
|
|
51
|
+
from graph_agents_cli._tools import ToolNotFoundError, require_tool
|
|
52
|
+
|
|
53
|
+
from ..utils import build_record, remote_template
|
|
54
|
+
from ..utils.backup import make_backup_pre_apply_hook
|
|
55
|
+
from ..utils.cli_options import shared_template_options
|
|
56
|
+
from ..utils.generation_metadata import metadata_to_cli_args
|
|
57
|
+
from ..utils.language import (
|
|
58
|
+
find_agent_file,
|
|
59
|
+
get_agent_file_hint,
|
|
60
|
+
get_language_config,
|
|
61
|
+
validate_agent_file,
|
|
62
|
+
)
|
|
63
|
+
from ..utils.logging import display_welcome_banner
|
|
64
|
+
from ..utils.manifest import (
|
|
65
|
+
CreateParams,
|
|
66
|
+
finalize_manifest,
|
|
67
|
+
recompute_secret_keys,
|
|
68
|
+
reconcile_secret_keys,
|
|
69
|
+
)
|
|
70
|
+
from ..utils.merge import (
|
|
71
|
+
run_three_way_merge,
|
|
72
|
+
)
|
|
73
|
+
from ..utils.template import (
|
|
74
|
+
default_checkpointer,
|
|
75
|
+
get_available_agents,
|
|
76
|
+
get_available_base_templates,
|
|
77
|
+
get_deployment_targets,
|
|
78
|
+
prompt_cd,
|
|
79
|
+
prompt_checkpointer,
|
|
80
|
+
prompt_deployment_target,
|
|
81
|
+
prompt_runtime,
|
|
82
|
+
validate_agent_directory_name,
|
|
83
|
+
validate_base_template,
|
|
84
|
+
validate_combination,
|
|
85
|
+
)
|
|
86
|
+
from ..utils.upgrade import (
|
|
87
|
+
Followup,
|
|
88
|
+
update_cli_metadata,
|
|
89
|
+
)
|
|
90
|
+
from ..utils.version import (
|
|
91
|
+
get_current_version,
|
|
92
|
+
pinned_install_spec,
|
|
93
|
+
pinned_spec_unavailable,
|
|
94
|
+
)
|
|
95
|
+
from .create import _git_origin_owner, create
|
|
96
|
+
|
|
97
|
+
console = Console()
|
|
98
|
+
|
|
99
|
+
# Environment variable names for saved config handling
|
|
100
|
+
_ENV_USING_SAVED_CONFIG = "_GRAPH_AGENTS_CLI_USING_SAVED_CONFIG"
|
|
101
|
+
_ENV_SKIP_VERSION_LOCK = "GRAPH_AGENTS_CLI_SKIP_VERSION_LOCK"
|
|
102
|
+
|
|
103
|
+
# Directories to exclude when scanning for agent directories
|
|
104
|
+
_EXCLUDED_DIRS = {
|
|
105
|
+
".git",
|
|
106
|
+
".github",
|
|
107
|
+
"__pycache__",
|
|
108
|
+
"node_modules",
|
|
109
|
+
".venv",
|
|
110
|
+
"venv",
|
|
111
|
+
"build",
|
|
112
|
+
"dist",
|
|
113
|
+
"deployment",
|
|
114
|
+
"tests",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# create_params keys an enhance override may change (all strings on the CLI).
|
|
118
|
+
_CREATE_PARAM_KEYS = (
|
|
119
|
+
"deployment_target",
|
|
120
|
+
"runtime",
|
|
121
|
+
"model_provider",
|
|
122
|
+
"model",
|
|
123
|
+
"checkpointer",
|
|
124
|
+
"registry",
|
|
125
|
+
"cd",
|
|
126
|
+
"auth_policy",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def model_after_provider_change(
|
|
131
|
+
project_config: ProjectConfig, new_provider: str, explicit_model: str | None = None
|
|
132
|
+
) -> str:
|
|
133
|
+
"""The model to record when ``--model-provider`` may change the provider.
|
|
134
|
+
|
|
135
|
+
An explicit ``--model`` wins. With the provider unchanged the recorded model
|
|
136
|
+
stays. When the provider changes and the recorded model is the old
|
|
137
|
+
provider's default (or unset), the new provider's default replaces it, as
|
|
138
|
+
``create`` would pick. A model the developer chose for the old provider
|
|
139
|
+
cannot be mapped: that is a usage error (exit 2) asking for ``--model``,
|
|
140
|
+
because keeping it would deploy one provider with another provider's model.
|
|
141
|
+
"""
|
|
142
|
+
if explicit_model:
|
|
143
|
+
return str(explicit_model)
|
|
144
|
+
old_provider = project_config.model_provider
|
|
145
|
+
recorded = project_config.model
|
|
146
|
+
if new_provider == old_provider:
|
|
147
|
+
return recorded
|
|
148
|
+
new_default = DEFAULT_MODELS.get(new_provider, "")
|
|
149
|
+
if recorded in ("", DEFAULT_MODELS.get(old_provider, "")):
|
|
150
|
+
return new_default
|
|
151
|
+
raise click.UsageError(
|
|
152
|
+
f"--model-provider {new_provider} changes the provider, but the recorded model "
|
|
153
|
+
f"{recorded!r} was chosen for {old_provider} and cannot be carried over.\n"
|
|
154
|
+
f" Pass --model <name> as well (the {new_provider} default is {new_default!r})."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def build_args_from_config(
|
|
159
|
+
project_config: ProjectConfig,
|
|
160
|
+
auto_approve: bool = False,
|
|
161
|
+
cli_overrides: dict[str, Any] | None = None,
|
|
162
|
+
cli_version: str | None = None,
|
|
163
|
+
) -> list[str]:
|
|
164
|
+
"""Build the ``scaffold enhance`` argv that replays the saved config plus overrides.
|
|
165
|
+
|
|
166
|
+
``cli_version`` is the CLI that will run the argv when it is not this one.
|
|
167
|
+
"""
|
|
168
|
+
# --skip-deps: dependencies were installed on first run; --skip-welcome: one banner
|
|
169
|
+
args = ["scaffold", "enhance", "--skip-deps", "--skip-welcome"]
|
|
170
|
+
|
|
171
|
+
if auto_approve:
|
|
172
|
+
args.append("--auto-approve")
|
|
173
|
+
|
|
174
|
+
args.extend(metadata_to_cli_args(project_config, for_enhance=True, cli_version=cli_version))
|
|
175
|
+
|
|
176
|
+
if cli_overrides:
|
|
177
|
+
for arg_name, value in cli_overrides.items():
|
|
178
|
+
cli_arg = f"--{arg_name.replace('_', '-')}"
|
|
179
|
+
_drop_flag(args, cli_arg)
|
|
180
|
+
if value is True:
|
|
181
|
+
args.append(cli_arg)
|
|
182
|
+
elif value is not False and value is not None:
|
|
183
|
+
args.extend([cli_arg, str(value)])
|
|
184
|
+
|
|
185
|
+
new_provider = cli_overrides.get("model_provider")
|
|
186
|
+
if new_provider and not cli_overrides.get("model"):
|
|
187
|
+
# The replayed --model belongs to the recorded provider.
|
|
188
|
+
_drop_flag(args, "--model")
|
|
189
|
+
model = model_after_provider_change(project_config, str(new_provider))
|
|
190
|
+
if model:
|
|
191
|
+
args.extend(["--model", model])
|
|
192
|
+
|
|
193
|
+
new_target = cli_overrides.get("deployment_target")
|
|
194
|
+
if new_target and new_target != project_config.deployment_target:
|
|
195
|
+
# A target change re-derives the checkpointer, cd and registry
|
|
196
|
+
# unless the caller pinned them; replaying the saved values would
|
|
197
|
+
# fail the combination table in the subprocess (memory on kubernetes).
|
|
198
|
+
for name in ("checkpointer", "cd", "registry"):
|
|
199
|
+
if name not in cli_overrides:
|
|
200
|
+
_drop_flag(args, f"--{name}")
|
|
201
|
+
|
|
202
|
+
return args
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _drop_flag(args: list[str], flag: str) -> None:
|
|
206
|
+
"""Remove every ``flag`` (and its value, when it has one) from ``args`` in place."""
|
|
207
|
+
i = 0
|
|
208
|
+
while i < len(args):
|
|
209
|
+
if args[i] == flag:
|
|
210
|
+
args.pop(i)
|
|
211
|
+
if i < len(args) and not args[i].startswith("--"):
|
|
212
|
+
args.pop(i)
|
|
213
|
+
else:
|
|
214
|
+
i += 1
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def get_display_params_from_config(project_config: ProjectConfig) -> dict[str, Any]:
|
|
218
|
+
"""Display-worthy parameters from the saved manifest."""
|
|
219
|
+
display_params: dict[str, Any] = {}
|
|
220
|
+
if project_config.base_template:
|
|
221
|
+
display_params["base_template"] = project_config.base_template
|
|
222
|
+
if project_config.agent_directory:
|
|
223
|
+
display_params["agent_directory"] = project_config.agent_directory
|
|
224
|
+
if project_config.cli_version:
|
|
225
|
+
display_params["cli_version"] = project_config.cli_version
|
|
226
|
+
for key, value in project_config.create_params.items():
|
|
227
|
+
if value is None or value == "":
|
|
228
|
+
continue
|
|
229
|
+
display_params[key] = value
|
|
230
|
+
return display_params
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _display_saved_config(
|
|
234
|
+
display_params: dict[str, Any],
|
|
235
|
+
project_version: str | None,
|
|
236
|
+
current_version: str,
|
|
237
|
+
use_different_version: bool,
|
|
238
|
+
) -> None:
|
|
239
|
+
"""Display detected saved configuration to the user."""
|
|
240
|
+
console.print()
|
|
241
|
+
console.print("📋 [bold]Detected saved configuration from previous setup:[/bold]")
|
|
242
|
+
console.print()
|
|
243
|
+
for key, value in display_params.items():
|
|
244
|
+
display_key = key.replace("_", " ").title()
|
|
245
|
+
console.print(f" • {display_key}: [cyan]{value}[/cyan]")
|
|
246
|
+
|
|
247
|
+
if use_different_version and project_version:
|
|
248
|
+
console.print()
|
|
249
|
+
console.print(f" • Version: [cyan]{project_version}[/cyan] (current: {current_version})")
|
|
250
|
+
console.print()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _should_use_different_version(project_version: str | None, current_version: str) -> bool:
|
|
254
|
+
"""Determine if we need to switch to a different CLI version."""
|
|
255
|
+
skip_version_lock = os.environ.get(_ENV_SKIP_VERSION_LOCK) == "1"
|
|
256
|
+
return (
|
|
257
|
+
not skip_version_lock
|
|
258
|
+
and bool(project_version)
|
|
259
|
+
and current_version != "0.0.0"
|
|
260
|
+
and project_version != current_version
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _ensure_uvx_available(project_version: str) -> None:
|
|
265
|
+
"""Ensure uvx is installed, exit with instructions if not."""
|
|
266
|
+
try:
|
|
267
|
+
require_tool("uvx")
|
|
268
|
+
except ToolNotFoundError:
|
|
269
|
+
console.print(
|
|
270
|
+
f"❌ Project requires graph-agents-cli version {project_version}, "
|
|
271
|
+
"but 'uvx' is not installed",
|
|
272
|
+
style="bold red",
|
|
273
|
+
)
|
|
274
|
+
console.print("💡 Install uv to use version-locked projects:", style="bold blue")
|
|
275
|
+
console.print(" curl -LsSf https://astral.sh/uv/install.sh | sh")
|
|
276
|
+
console.print(" OR visit: https://docs.astral.sh/uv/getting-started/installation/")
|
|
277
|
+
sys.exit(2) # a missing tool, like every other command
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _execute_with_saved_config(
|
|
281
|
+
args: list[str], project_version: str | None, use_different_version: bool
|
|
282
|
+
) -> bool:
|
|
283
|
+
"""Execute enhance with saved config args (in a subprocess); True on success."""
|
|
284
|
+
if use_different_version and project_version:
|
|
285
|
+
spec = pinned_install_spec(project_version)
|
|
286
|
+
if spec is None:
|
|
287
|
+
# Running the override would pass off another build as project_version.
|
|
288
|
+
console.print(f"⚠️ {pinned_spec_unavailable(project_version)}.", style="yellow")
|
|
289
|
+
console.print(
|
|
290
|
+
"⚠️ Continuing with current version, but compatibility is not guaranteed",
|
|
291
|
+
style="yellow",
|
|
292
|
+
)
|
|
293
|
+
return False
|
|
294
|
+
console.print(f"📦 Using graph-agents-cli version {project_version}...", style="dim")
|
|
295
|
+
_ensure_uvx_available(project_version)
|
|
296
|
+
cmd = ["uvx", "--from", spec, "graph-agents-cli", *args]
|
|
297
|
+
else:
|
|
298
|
+
console.print("✅ Using saved configuration", style="dim")
|
|
299
|
+
cmd = [sys.executable, "-m", "graph_agents_cli.main", *args]
|
|
300
|
+
|
|
301
|
+
logging.debug("Executing command: %s", shlex.join(cmd))
|
|
302
|
+
|
|
303
|
+
env = os.environ.copy()
|
|
304
|
+
env[_ENV_USING_SAVED_CONFIG] = "1"
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
run_resolved(cmd, check=True, env=env)
|
|
308
|
+
return True
|
|
309
|
+
except subprocess.CalledProcessError as e:
|
|
310
|
+
if not use_different_version:
|
|
311
|
+
# This CLI already ran the saved configuration and said why it
|
|
312
|
+
# stopped. Falling back would only repeat the same enhance in process
|
|
313
|
+
# (after the manifest was rewritten, so it could even report
|
|
314
|
+
# success): keep its exit code, e.g. 1 when required items are left
|
|
315
|
+
# (128 + N when a signal N ended it).
|
|
316
|
+
code = e.returncode if e.returncode > 0 else 128 - e.returncode
|
|
317
|
+
raise click.exceptions.Exit(code if e.returncode else 1) from e
|
|
318
|
+
console.print(
|
|
319
|
+
f"❌ Failed to execute with locked version {project_version}: {e}",
|
|
320
|
+
style="bold red",
|
|
321
|
+
)
|
|
322
|
+
console.print(
|
|
323
|
+
"⚠️ Continuing with current version, but compatibility is not guaranteed",
|
|
324
|
+
style="yellow",
|
|
325
|
+
)
|
|
326
|
+
return False
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def check_and_execute_with_saved_config(
|
|
330
|
+
*,
|
|
331
|
+
project_dir: pathlib.Path,
|
|
332
|
+
auto_approve: bool = False,
|
|
333
|
+
cli_overrides: dict[str, Any] | None = None,
|
|
334
|
+
force: bool = False,
|
|
335
|
+
dry_run: bool = False,
|
|
336
|
+
interactive: bool = False,
|
|
337
|
+
) -> bool | dict[str, Any]:
|
|
338
|
+
"""Check for saved config and offer to reuse it.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
True if config was used and executed successfully.
|
|
342
|
+
False if no saved config found or execution failed.
|
|
343
|
+
dict if the user customized interactively: only the changed parameters.
|
|
344
|
+
"""
|
|
345
|
+
if os.environ.get(_ENV_USING_SAVED_CONFIG) == "1":
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
project_config = find_project_config(project_dir)
|
|
349
|
+
if not project_config:
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
display_params = get_display_params_from_config(project_config)
|
|
353
|
+
if not display_params:
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
current_version = get_current_version()
|
|
357
|
+
project_version = project_config.cli_version
|
|
358
|
+
use_different_version = _should_use_different_version(project_version, current_version)
|
|
359
|
+
|
|
360
|
+
_display_saved_config(display_params, project_version, current_version, use_different_version)
|
|
361
|
+
|
|
362
|
+
if interactive:
|
|
363
|
+
return _prompt_customize_overrides(project_config)
|
|
364
|
+
|
|
365
|
+
args = build_args_from_config(
|
|
366
|
+
project_config,
|
|
367
|
+
auto_approve,
|
|
368
|
+
cli_overrides,
|
|
369
|
+
cli_version=project_version if use_different_version else None,
|
|
370
|
+
)
|
|
371
|
+
is_older_version = (
|
|
372
|
+
use_different_version
|
|
373
|
+
and bool(project_version)
|
|
374
|
+
and pkg_version.parse(project_version) < pkg_version.parse(current_version)
|
|
375
|
+
)
|
|
376
|
+
if not is_older_version:
|
|
377
|
+
if force:
|
|
378
|
+
args.append("--force")
|
|
379
|
+
if dry_run:
|
|
380
|
+
args.append("--dry-run")
|
|
381
|
+
return _execute_with_saved_config(args, project_version, use_different_version)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _prompt_customize_overrides(project_config: ProjectConfig) -> dict[str, Any]:
|
|
385
|
+
"""Prompt for the parameters enhance can change; return only the changed ones."""
|
|
386
|
+
overrides: dict[str, Any] = {}
|
|
387
|
+
|
|
388
|
+
base_template = project_config.base_template
|
|
389
|
+
new_agent = display_base_template_selection(base_template)
|
|
390
|
+
if new_agent != base_template:
|
|
391
|
+
overrides["base_template"] = new_agent
|
|
392
|
+
effective_agent = new_agent
|
|
393
|
+
|
|
394
|
+
available_targets = get_deployment_targets(effective_agent)
|
|
395
|
+
current_deployment = project_config.deployment_target
|
|
396
|
+
if available_targets and len(available_targets) > 1:
|
|
397
|
+
new_deployment = prompt_deployment_target(effective_agent, default_value=current_deployment)
|
|
398
|
+
elif available_targets:
|
|
399
|
+
new_deployment = available_targets[0]
|
|
400
|
+
else:
|
|
401
|
+
new_deployment = current_deployment
|
|
402
|
+
if new_deployment != current_deployment:
|
|
403
|
+
overrides["deployment_target"] = new_deployment
|
|
404
|
+
|
|
405
|
+
current_runtime = project_config.runtime
|
|
406
|
+
new_runtime = prompt_runtime(current_runtime)
|
|
407
|
+
if new_runtime != current_runtime:
|
|
408
|
+
overrides["runtime"] = new_runtime
|
|
409
|
+
|
|
410
|
+
current_checkpointer = project_config.checkpointer
|
|
411
|
+
suggested = (
|
|
412
|
+
current_checkpointer
|
|
413
|
+
if new_deployment == current_deployment
|
|
414
|
+
else default_checkpointer(new_deployment)
|
|
415
|
+
)
|
|
416
|
+
new_checkpointer = prompt_checkpointer(suggested)
|
|
417
|
+
if new_checkpointer != current_checkpointer:
|
|
418
|
+
overrides["checkpointer"] = new_checkpointer
|
|
419
|
+
|
|
420
|
+
current_cd = project_config.cd
|
|
421
|
+
if new_deployment == "kubernetes":
|
|
422
|
+
new_cd = prompt_cd(current_cd)
|
|
423
|
+
else:
|
|
424
|
+
new_cd = "skip"
|
|
425
|
+
if new_cd != current_cd:
|
|
426
|
+
overrides["cd"] = new_cd
|
|
427
|
+
|
|
428
|
+
console.print()
|
|
429
|
+
return overrides
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def display_base_template_selection(current_base: str) -> str:
|
|
433
|
+
"""Display available base templates and prompt for selection."""
|
|
434
|
+
agents = get_available_agents()
|
|
435
|
+
|
|
436
|
+
if not agents:
|
|
437
|
+
raise click.ClickException("No base templates available")
|
|
438
|
+
|
|
439
|
+
console.print()
|
|
440
|
+
console.print("🔧 [bold]Base Template Selection[/bold]")
|
|
441
|
+
console.print()
|
|
442
|
+
console.print(f"Your project currently inherits from: [cyan]{current_base}[/cyan]")
|
|
443
|
+
console.print("Available base templates:")
|
|
444
|
+
|
|
445
|
+
template_choices = {}
|
|
446
|
+
choice_num = 1
|
|
447
|
+
current_choice = None
|
|
448
|
+
|
|
449
|
+
for agent in agents.values():
|
|
450
|
+
template_choices[choice_num] = agent["name"]
|
|
451
|
+
if agent["name"] == current_base:
|
|
452
|
+
console.print(
|
|
453
|
+
f" {choice_num}. [bold cyan]{agent['name']}[/]"
|
|
454
|
+
f" [dim]{agent['description']}[/]"
|
|
455
|
+
" [dim cyan](current)[/]"
|
|
456
|
+
)
|
|
457
|
+
current_choice = choice_num
|
|
458
|
+
else:
|
|
459
|
+
console.print(f" [dim]{choice_num}. {agent['name']} - {agent['description']}[/]")
|
|
460
|
+
choice_num += 1
|
|
461
|
+
|
|
462
|
+
if current_choice is None:
|
|
463
|
+
current_choice = 1
|
|
464
|
+
|
|
465
|
+
console.print()
|
|
466
|
+
choice = IntPrompt.ask("Select base template", default=current_choice, show_default=True)
|
|
467
|
+
|
|
468
|
+
if choice in template_choices:
|
|
469
|
+
return template_choices[choice]
|
|
470
|
+
raise ValueError(f"Invalid base template selection: {choice}")
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def display_agent_directory_selection(
|
|
474
|
+
current_dir: pathlib.Path,
|
|
475
|
+
detected_directory: str,
|
|
476
|
+
) -> str:
|
|
477
|
+
"""Display available directories and prompt for agent directory selection."""
|
|
478
|
+
agent_file_hint = get_language_config("python")["agent_file"]
|
|
479
|
+
while True:
|
|
480
|
+
console.print()
|
|
481
|
+
console.print("📁 [bold]Agent Directory Selection[/bold]")
|
|
482
|
+
console.print()
|
|
483
|
+
console.print("Your project needs an agent directory containing:")
|
|
484
|
+
console.print(f" • [cyan]{agent_file_hint}[/cyan] exporting the compiled graph as `graph`")
|
|
485
|
+
console.print()
|
|
486
|
+
console.print("Choose where your agent code is located:")
|
|
487
|
+
|
|
488
|
+
available_dirs = sorted(
|
|
489
|
+
item.name
|
|
490
|
+
for item in current_dir.iterdir()
|
|
491
|
+
if item.is_dir() and not item.name.startswith(".") and item.name not in _EXCLUDED_DIRS
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
directory_choices = {}
|
|
495
|
+
choice_num = 1
|
|
496
|
+
default_choice = None
|
|
497
|
+
|
|
498
|
+
if detected_directory in available_dirs:
|
|
499
|
+
directory_choices[choice_num] = detected_directory
|
|
500
|
+
current_indicator = " (detected)" if detected_directory != "app" else " (default)"
|
|
501
|
+
console.print(f" {choice_num}. [bold]{detected_directory}[/]{current_indicator}")
|
|
502
|
+
default_choice = choice_num
|
|
503
|
+
choice_num += 1
|
|
504
|
+
available_dirs.remove(detected_directory)
|
|
505
|
+
|
|
506
|
+
for dir_name in available_dirs:
|
|
507
|
+
directory_choices[choice_num] = dir_name
|
|
508
|
+
hint = get_agent_file_hint(current_dir / dir_name)
|
|
509
|
+
console.print(f" {choice_num}. [bold]{dir_name}[/]{hint}")
|
|
510
|
+
if default_choice is None:
|
|
511
|
+
default_choice = choice_num
|
|
512
|
+
choice_num += 1
|
|
513
|
+
|
|
514
|
+
custom_choice = choice_num
|
|
515
|
+
directory_choices[custom_choice] = "__custom__"
|
|
516
|
+
console.print(f" {custom_choice}. [bold]Enter custom directory name[/]")
|
|
517
|
+
|
|
518
|
+
if default_choice is None:
|
|
519
|
+
default_choice = custom_choice
|
|
520
|
+
|
|
521
|
+
console.print()
|
|
522
|
+
choice = IntPrompt.ask("Select agent directory", default=default_choice, show_default=True)
|
|
523
|
+
|
|
524
|
+
if choice not in directory_choices:
|
|
525
|
+
console.print(f"[bold red]Error:[/] Invalid selection: {choice}", style="bold red")
|
|
526
|
+
console.print("Please choose a valid option from the list.")
|
|
527
|
+
console.print()
|
|
528
|
+
continue
|
|
529
|
+
|
|
530
|
+
selected = directory_choices[choice]
|
|
531
|
+
if selected == "__custom__":
|
|
532
|
+
console.print()
|
|
533
|
+
while True:
|
|
534
|
+
custom_dir = Prompt.ask(
|
|
535
|
+
"Enter custom agent directory name", default=detected_directory
|
|
536
|
+
)
|
|
537
|
+
try:
|
|
538
|
+
validate_agent_directory_name(custom_dir)
|
|
539
|
+
return custom_dir
|
|
540
|
+
except ValueError as e:
|
|
541
|
+
console.print(f"[bold red]Error:[/] {e}", style="bold red")
|
|
542
|
+
console.print("Please try again with a valid directory name.")
|
|
543
|
+
try:
|
|
544
|
+
validate_agent_directory_name(selected)
|
|
545
|
+
return selected
|
|
546
|
+
except ValueError as e:
|
|
547
|
+
console.print(f"[bold red]Error:[/] {e}", style="bold red")
|
|
548
|
+
console.print(
|
|
549
|
+
"This directory cannot be used as an agent directory. Please select another option."
|
|
550
|
+
)
|
|
551
|
+
console.print()
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _build_enhance_create_args(
|
|
555
|
+
project_config: ProjectConfig,
|
|
556
|
+
cli_overrides: dict[str, Any] | None = None,
|
|
557
|
+
project_dir: pathlib.Path | None = None,
|
|
558
|
+
) -> list[str]:
|
|
559
|
+
"""Build ``create`` args for the enhanced snapshot from the effective parameters.
|
|
560
|
+
|
|
561
|
+
Every create parameter comes from ``_effective_params`` (the values the
|
|
562
|
+
combination check validated), so a target change renders with the
|
|
563
|
+
checkpointer, cd and registry it re-derives instead of replaying the saved ones.
|
|
564
|
+
"""
|
|
565
|
+
overrides = cli_overrides or {}
|
|
566
|
+
params = _effective_params(project_config, cli_overrides, project_dir)
|
|
567
|
+
args: list[str] = []
|
|
568
|
+
|
|
569
|
+
base_template = overrides.get("base_template") or project_config.base_template
|
|
570
|
+
if base_template:
|
|
571
|
+
args.extend(["--agent", str(base_template)])
|
|
572
|
+
agent_directory = overrides.get("agent_directory") or project_config.agent_directory
|
|
573
|
+
if agent_directory and agent_directory != "app":
|
|
574
|
+
args.extend(["--agent-directory", str(agent_directory)])
|
|
575
|
+
guidance = overrides.get("agent_guidance_filename") or project_config.agent_guidance_filename
|
|
576
|
+
args.extend(["--agent-guidance-filename", str(guidance)])
|
|
577
|
+
|
|
578
|
+
args.extend(["--deployment-target", params.deployment_target])
|
|
579
|
+
args.extend(["--runtime", params.runtime])
|
|
580
|
+
args.extend(["--model-provider", params.model_provider])
|
|
581
|
+
if params.model:
|
|
582
|
+
args.extend(["--model", params.model])
|
|
583
|
+
args.extend(["--checkpointer", params.checkpointer])
|
|
584
|
+
if params.registry:
|
|
585
|
+
args.extend(["--registry", params.registry])
|
|
586
|
+
args.extend(["--cd", params.cd])
|
|
587
|
+
args.extend(["--auth-policy", params.auth_policy])
|
|
588
|
+
if params.process:
|
|
589
|
+
args.extend(["--process", params.process])
|
|
590
|
+
if overrides.get("prototype"):
|
|
591
|
+
args.append("--prototype")
|
|
592
|
+
return args
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _effective_params(
|
|
596
|
+
project_config: ProjectConfig,
|
|
597
|
+
cli_overrides: dict[str, Any] | None,
|
|
598
|
+
project_dir: pathlib.Path | None = None,
|
|
599
|
+
) -> CreateParams:
|
|
600
|
+
"""The create parameters after applying ``cli_overrides`` to the saved config.
|
|
601
|
+
|
|
602
|
+
A target change re-derives the checkpointer (postgres for kubernetes,
|
|
603
|
+
memory for none) and the registry unless overridden. An explicit ``--cd`` is kept as given so
|
|
604
|
+
``validate_combination`` enforces the kubernetes rule the way ``create``
|
|
605
|
+
does (``--prototype`` forces ``skip``, also like ``create``); only the saved
|
|
606
|
+
cd is forced to ``skip`` when the target leaves kubernetes. The recorded
|
|
607
|
+
``auth_policy_implemented`` is carried through while the policy is unchanged.
|
|
608
|
+
"""
|
|
609
|
+
overrides = cli_overrides or {}
|
|
610
|
+
target = str(overrides.get("deployment_target", project_config.deployment_target))
|
|
611
|
+
runtime = str(overrides.get("runtime", project_config.runtime))
|
|
612
|
+
checkpointer = overrides.get("checkpointer")
|
|
613
|
+
if not checkpointer:
|
|
614
|
+
checkpointer = (
|
|
615
|
+
project_config.checkpointer
|
|
616
|
+
if target == project_config.deployment_target
|
|
617
|
+
else default_checkpointer(target)
|
|
618
|
+
)
|
|
619
|
+
explicit_cd = overrides.get("cd")
|
|
620
|
+
if overrides.get("prototype"):
|
|
621
|
+
cd = "skip"
|
|
622
|
+
elif explicit_cd:
|
|
623
|
+
cd = str(explicit_cd)
|
|
624
|
+
elif target == "kubernetes":
|
|
625
|
+
cd = str(project_config.cd)
|
|
626
|
+
else:
|
|
627
|
+
cd = "skip"
|
|
628
|
+
if target == "kubernetes":
|
|
629
|
+
registry = str(overrides.get("registry") or project_config.registry or "")
|
|
630
|
+
if not registry:
|
|
631
|
+
registry = _default_registry(project_dir)
|
|
632
|
+
else:
|
|
633
|
+
registry = ""
|
|
634
|
+
auth_policy = str(overrides.get("auth_policy", project_config.auth_policy))
|
|
635
|
+
implemented = (
|
|
636
|
+
project_config.auth_policy_implemented
|
|
637
|
+
if auth_policy == project_config.auth_policy
|
|
638
|
+
else None
|
|
639
|
+
)
|
|
640
|
+
has_policy = bool(project_config.api_policy_file)
|
|
641
|
+
apis: tuple[_api_policy.ApiSummary, ...] = ()
|
|
642
|
+
if has_policy and project_dir is not None:
|
|
643
|
+
apis = _api_policy.read_summaries(project_dir / str(project_config.api_policy_file))
|
|
644
|
+
process = overrides.get("process", project_config.process)
|
|
645
|
+
model_provider = str(overrides.get("model_provider", project_config.model_provider))
|
|
646
|
+
return CreateParams(
|
|
647
|
+
deployment_target=target,
|
|
648
|
+
runtime=runtime,
|
|
649
|
+
model_provider=model_provider,
|
|
650
|
+
model=model_after_provider_change(project_config, model_provider, overrides.get("model")),
|
|
651
|
+
checkpointer=str(checkpointer),
|
|
652
|
+
registry=registry,
|
|
653
|
+
cd=cd,
|
|
654
|
+
auth_policy=auth_policy,
|
|
655
|
+
has_api_policy=has_policy,
|
|
656
|
+
process=str(process) if process else None,
|
|
657
|
+
auth_policy_implemented=implemented,
|
|
658
|
+
apis=apis,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _default_registry(project_dir: pathlib.Path | None) -> str:
|
|
663
|
+
"""The registry ``create`` would pick for a project that gains the kubernetes target."""
|
|
664
|
+
owner = _git_origin_owner(project_dir) if project_dir is not None else None
|
|
665
|
+
return f"{DEFAULT_REGISTRY_HOST}/{owner}" if owner else DEFAULT_REGISTRY_PLACEHOLDER
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _validate_effective_params(
|
|
669
|
+
params: CreateParams, project_dir: pathlib.Path | None = None
|
|
670
|
+
) -> None:
|
|
671
|
+
"""Refuse (usage error, exit 2) what ``create`` would refuse for the same parameters.
|
|
672
|
+
|
|
673
|
+
That includes the project's own ``api-policy.yaml``: an ``auth: forward``
|
|
674
|
+
API cannot move to the langgraph-server runtime, which persists the run
|
|
675
|
+
context and so would store the forwarded credentials.
|
|
676
|
+
"""
|
|
677
|
+
try:
|
|
678
|
+
validate_combination(
|
|
679
|
+
params.runtime, params.checkpointer, params.deployment_target, params.cd
|
|
680
|
+
)
|
|
681
|
+
except ValueError as e:
|
|
682
|
+
raise click.UsageError(str(e)) from e
|
|
683
|
+
apis = params.apis
|
|
684
|
+
if not apis and project_dir is not None:
|
|
685
|
+
apis = _api_policy.read_summaries(project_dir / API_POLICY_FILENAME)
|
|
686
|
+
problem = _api_policy.forward_runtime_problem(apis, params.runtime)
|
|
687
|
+
if problem:
|
|
688
|
+
raise click.UsageError(f"{problem} (in {API_POLICY_FILENAME})")
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _backfill_create_params_from_config(
|
|
692
|
+
current_dir: pathlib.Path,
|
|
693
|
+
cli_params: dict[str, Any],
|
|
694
|
+
) -> dict[str, Any]:
|
|
695
|
+
"""Fill None CLI create params from the saved manifest; CLI values always win.
|
|
696
|
+
|
|
697
|
+
When the CLI changes the deployment target, the saved checkpointer, cd and
|
|
698
|
+
registry are not carried over: the checkpointer follows the new target's
|
|
699
|
+
default and ``create`` derives cd and registry.
|
|
700
|
+
"""
|
|
701
|
+
config = find_project_config(current_dir)
|
|
702
|
+
if not config:
|
|
703
|
+
return cli_params
|
|
704
|
+
|
|
705
|
+
saved = config.create_params
|
|
706
|
+
if not saved:
|
|
707
|
+
return cli_params
|
|
708
|
+
|
|
709
|
+
result = cli_params.copy()
|
|
710
|
+
new_target = result.get("deployment_target")
|
|
711
|
+
target_changed = bool(new_target) and new_target != saved.get("deployment_target")
|
|
712
|
+
for key in result:
|
|
713
|
+
if result[key] is not None:
|
|
714
|
+
continue
|
|
715
|
+
if target_changed and key in ("checkpointer", "cd", "registry"):
|
|
716
|
+
if key == "checkpointer":
|
|
717
|
+
result[key] = default_checkpointer(str(new_target))
|
|
718
|
+
continue
|
|
719
|
+
if key in saved and saved[key] not in (None, ""):
|
|
720
|
+
result[key] = saved[key]
|
|
721
|
+
if result.get("auth_policy"):
|
|
722
|
+
# A manifest may still record a retired policy name.
|
|
723
|
+
result["auth_policy"] = normalize_auth_policy(str(result["auth_policy"]))
|
|
724
|
+
if cli_params.get("model_provider") and not cli_params.get("model"):
|
|
725
|
+
# The saved model belongs to the saved provider.
|
|
726
|
+
result["model"] = model_after_provider_change(config, str(cli_params["model_provider"]))
|
|
727
|
+
return result
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _run_smart_merge(
|
|
731
|
+
*,
|
|
732
|
+
project_dir: pathlib.Path,
|
|
733
|
+
project_config: ProjectConfig,
|
|
734
|
+
cli_overrides: dict[str, Any] | None,
|
|
735
|
+
auto_approve: bool,
|
|
736
|
+
dry_run: bool,
|
|
737
|
+
prefer_new: bool = False,
|
|
738
|
+
interactive: bool = False,
|
|
739
|
+
) -> bool:
|
|
740
|
+
"""Smart-merge: 3-way comparison between the recorded render and the enhanced one.
|
|
741
|
+
|
|
742
|
+
Returns True if smart-merge completed successfully, False otherwise.
|
|
743
|
+
"""
|
|
744
|
+
project_name = project_config.project_name or project_dir.name
|
|
745
|
+
agent_directory = project_config.agent_directory
|
|
746
|
+
language = project_config.language
|
|
747
|
+
|
|
748
|
+
params = _effective_params(project_config, cli_overrides, project_dir)
|
|
749
|
+
_validate_effective_params(params, project_dir)
|
|
750
|
+
previous = _recorded_params(project_config, project_dir)
|
|
751
|
+
_print_recomputed_settings(project_config, previous, params)
|
|
752
|
+
|
|
753
|
+
old_args = metadata_to_cli_args(project_config)
|
|
754
|
+
new_args = _build_enhance_create_args(project_config, cli_overrides, project_dir)
|
|
755
|
+
|
|
756
|
+
backup_hook = make_backup_pre_apply_hook(
|
|
757
|
+
console=console,
|
|
758
|
+
auto_approve=auto_approve,
|
|
759
|
+
interactive=interactive,
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
current_build = build_record.running_build()
|
|
763
|
+
try:
|
|
764
|
+
recorded_build = build_record.recorded_build_for(project_config)
|
|
765
|
+
except build_record.MalformedRecordError as e:
|
|
766
|
+
console.print(f"[yellow]⚠️ {escape(str(e))}; it is ignored.[/yellow]")
|
|
767
|
+
recorded_build = None
|
|
768
|
+
digests: dict[str, str] = {}
|
|
769
|
+
|
|
770
|
+
def _check_snapshots(old_dir: pathlib.Path, new_dir: pathlib.Path) -> None:
|
|
771
|
+
# Both snapshots are this build's. The project is at this build's
|
|
772
|
+
# templates when it was rendered by this very build, or by one that
|
|
773
|
+
# renders its settings identically; otherwise files the template changed
|
|
774
|
+
# since its build look like the developer's edits here.
|
|
775
|
+
digests["old"] = build_record.template_digest(old_dir)
|
|
776
|
+
digests["new"] = build_record.template_digest(new_dir)
|
|
777
|
+
if recorded_build is not None and not _at_this_build(recorded_build, digests["old"]):
|
|
778
|
+
console.print(
|
|
779
|
+
f"[yellow]⚠️ This project was rendered by build {escape(recorded_build.id)}, whose "
|
|
780
|
+
f"templates differ from this build's ({current_build.id}): enhance compares "
|
|
781
|
+
"your files with this build's templates, so a file the template changed since "
|
|
782
|
+
"then counts as your edit (kept, or listed as a conflict). Run "
|
|
783
|
+
"`graph-agents-cli scaffold upgrade` to bring those changes in.[/yellow]"
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
def _at_this_build(record: build_record.BuildRecord, old_digest: str) -> bool:
|
|
787
|
+
if record.same_build_as(current_build):
|
|
788
|
+
return True
|
|
789
|
+
return record.template_digest is not None and record.template_digest == old_digest
|
|
790
|
+
|
|
791
|
+
def _record_build(proj_dir: pathlib.Path) -> None:
|
|
792
|
+
"""The new settings' snapshot is this build's; the project is at it only if it was before."""
|
|
793
|
+
if recorded_build is None or "new" not in digests:
|
|
794
|
+
return # a manifest without cli_build stays compared by version only
|
|
795
|
+
if _at_this_build(recorded_build, digests["old"]):
|
|
796
|
+
record = build_record.BuildRecord.of(current_build, digests["new"])
|
|
797
|
+
else:
|
|
798
|
+
# Still the recorded build's files; its digest described the old settings.
|
|
799
|
+
record = build_record.BuildRecord(recorded_build.id, recorded_build.commit, None)
|
|
800
|
+
build_record.write_build_record(proj_dir, record)
|
|
801
|
+
|
|
802
|
+
def _update_metadata(proj_dir: pathlib.Path, lang: str) -> list[str | Followup] | None:
|
|
803
|
+
if not cli_overrides:
|
|
804
|
+
return None
|
|
805
|
+
has_policy = params.has_api_policy or (proj_dir / API_POLICY_FILENAME).is_file()
|
|
806
|
+
apis = _api_policy.read_summaries(proj_dir / API_POLICY_FILENAME) if has_policy else ()
|
|
807
|
+
current = dataclasses.replace(params, has_api_policy=has_policy, apis=apis)
|
|
808
|
+
finalize_manifest(
|
|
809
|
+
proj_dir,
|
|
810
|
+
project_name=project_name,
|
|
811
|
+
# dataclasses.replace keeps auth_policy_implemented and every other
|
|
812
|
+
# field of the validated params; only the policy facts are refreshed.
|
|
813
|
+
params=current,
|
|
814
|
+
cli_version=get_current_version(),
|
|
815
|
+
)
|
|
816
|
+
added, removed = reconcile_secret_keys(proj_dir, previous=previous, current=current)
|
|
817
|
+
extra: dict[str, Any] = {}
|
|
818
|
+
if cli_overrides.get("agent_guidance_filename"):
|
|
819
|
+
extra["agent_guidance_filename"] = cli_overrides["agent_guidance_filename"]
|
|
820
|
+
if extra:
|
|
821
|
+
update_cli_metadata(proj_dir, extra)
|
|
822
|
+
_record_build(proj_dir)
|
|
823
|
+
return [
|
|
824
|
+
*_settings_followups(proj_dir, previous, current, added, removed),
|
|
825
|
+
*_chart_followups(proj_dir, previous, current),
|
|
826
|
+
]
|
|
827
|
+
|
|
828
|
+
return run_three_way_merge(
|
|
829
|
+
project_dir=project_dir,
|
|
830
|
+
project_name=project_name,
|
|
831
|
+
agent_directory=agent_directory,
|
|
832
|
+
language=language,
|
|
833
|
+
old_args=old_args,
|
|
834
|
+
new_args=new_args,
|
|
835
|
+
auto_approve=auto_approve,
|
|
836
|
+
dry_run=dry_run,
|
|
837
|
+
prefer_new=prefer_new,
|
|
838
|
+
interactive=interactive,
|
|
839
|
+
operation_label="enhancement",
|
|
840
|
+
pre_apply_hook=backup_hook,
|
|
841
|
+
post_apply_hook=_update_metadata,
|
|
842
|
+
merge_config=True,
|
|
843
|
+
snapshot_check=_check_snapshots,
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def _recorded_params(project_config: ProjectConfig, project_dir: pathlib.Path) -> CreateParams:
|
|
848
|
+
"""The create parameters the manifest records (before an enhance changes them)."""
|
|
849
|
+
apis: tuple[_api_policy.ApiSummary, ...] = ()
|
|
850
|
+
if project_config.api_policy_file:
|
|
851
|
+
apis = _api_policy.read_summaries(project_dir / str(project_config.api_policy_file))
|
|
852
|
+
return CreateParams(
|
|
853
|
+
deployment_target=project_config.deployment_target,
|
|
854
|
+
runtime=project_config.runtime,
|
|
855
|
+
model_provider=project_config.model_provider,
|
|
856
|
+
model=project_config.model,
|
|
857
|
+
checkpointer=project_config.checkpointer,
|
|
858
|
+
registry=project_config.registry,
|
|
859
|
+
cd=project_config.cd,
|
|
860
|
+
auth_policy=project_config.auth_policy,
|
|
861
|
+
has_api_policy=bool(project_config.api_policy_file),
|
|
862
|
+
apis=apis,
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _print_recomputed_settings(
|
|
867
|
+
project_config: ProjectConfig, previous: CreateParams, params: CreateParams
|
|
868
|
+
) -> None:
|
|
869
|
+
"""Say which recorded settings the change recomputes (before anything is written)."""
|
|
870
|
+
lines: list[str] = []
|
|
871
|
+
for label, old, new in (
|
|
872
|
+
("runtime", previous.runtime, params.runtime),
|
|
873
|
+
("model_provider", previous.model_provider, params.model_provider),
|
|
874
|
+
("model", previous.model, params.model),
|
|
875
|
+
):
|
|
876
|
+
if old != new:
|
|
877
|
+
lines.append(f"{label}: {old} -> {new}")
|
|
878
|
+
old_defaults = default_secret_keys(
|
|
879
|
+
previous.model_provider,
|
|
880
|
+
previous.runtime,
|
|
881
|
+
previous.api_token_envs,
|
|
882
|
+
auth_policy=previous.auth_policy,
|
|
883
|
+
)
|
|
884
|
+
new_defaults = default_secret_keys(
|
|
885
|
+
params.model_provider,
|
|
886
|
+
params.runtime,
|
|
887
|
+
params.api_token_envs,
|
|
888
|
+
auth_policy=params.auth_policy,
|
|
889
|
+
)
|
|
890
|
+
recorded = project_config.secret_keys
|
|
891
|
+
keys = recompute_secret_keys(recorded, old_defaults, new_defaults)
|
|
892
|
+
added = [k for k in keys if k not in recorded]
|
|
893
|
+
removed = [k for k in recorded if k not in keys]
|
|
894
|
+
if added or removed:
|
|
895
|
+
change = ", ".join([*(f"+{k}" for k in added), *(f"-{k}" for k in removed)])
|
|
896
|
+
lines.append(f"secrets.keys: {change} (keys you added are kept)")
|
|
897
|
+
if not lines:
|
|
898
|
+
return
|
|
899
|
+
console.print()
|
|
900
|
+
console.print("[bold]Recomputed for the new settings (graph-agents-cli-manifest.yaml):[/bold]")
|
|
901
|
+
for line in lines:
|
|
902
|
+
console.print(f" • {line}")
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def _settings_followups(
|
|
906
|
+
project_dir: pathlib.Path,
|
|
907
|
+
previous: CreateParams,
|
|
908
|
+
current: CreateParams,
|
|
909
|
+
keys_added: list[str],
|
|
910
|
+
keys_removed: list[str],
|
|
911
|
+
) -> list[str]:
|
|
912
|
+
"""What the developer still has to do by hand after a provider or runtime change.
|
|
913
|
+
|
|
914
|
+
``.env`` holds the developer's own values and is never edited by enhance,
|
|
915
|
+
and the environments' Secrets live in the cluster: both are listed here.
|
|
916
|
+
"""
|
|
917
|
+
items: list[str] = []
|
|
918
|
+
env_values: dict[str, str | None] = {}
|
|
919
|
+
env_file = project_dir / ".env"
|
|
920
|
+
if env_file.is_file():
|
|
921
|
+
from dotenv import dotenv_values
|
|
922
|
+
|
|
923
|
+
try:
|
|
924
|
+
env_values = dict(dotenv_values(env_file))
|
|
925
|
+
except Exception: # an unreadable .env is reported by run/eval themselves
|
|
926
|
+
env_values = {}
|
|
927
|
+
if previous.model_provider != current.model_provider or previous.model != current.model:
|
|
928
|
+
stale = [
|
|
929
|
+
f"{name}={env_values.get(name)}"
|
|
930
|
+
for name, want in (
|
|
931
|
+
("MODEL_PROVIDER", current.model_provider),
|
|
932
|
+
("MODEL_NAME", current.model),
|
|
933
|
+
)
|
|
934
|
+
if env_values.get(name) not in (None, "", want)
|
|
935
|
+
]
|
|
936
|
+
if stale:
|
|
937
|
+
items.append(
|
|
938
|
+
f".env still sets {' '.join(stale)}: change it to "
|
|
939
|
+
f"MODEL_PROVIDER={current.model_provider} MODEL_NAME={current.model} "
|
|
940
|
+
"(run, playground and eval read .env)"
|
|
941
|
+
)
|
|
942
|
+
key_var = PROVIDER_KEY_VARS.get(current.model_provider, "MODEL_API_KEY")
|
|
943
|
+
if previous.model_provider != current.model_provider and not env_values.get(key_var):
|
|
944
|
+
items.append(
|
|
945
|
+
f"Set {key_var} in .env (and in each environment's env file, e.g. "
|
|
946
|
+
f".env.staging, before `secrets apply`)"
|
|
947
|
+
)
|
|
948
|
+
if previous.runtime != current.runtime and current.deployment_target == "kubernetes":
|
|
949
|
+
needs = (
|
|
950
|
+
"DATABASE_URI and REDIS_URI"
|
|
951
|
+
if current.runtime == "langgraph-server"
|
|
952
|
+
else "POSTGRES_DSN"
|
|
953
|
+
)
|
|
954
|
+
items.append(
|
|
955
|
+
f"The {current.runtime} runtime reads {needs} in staging and prod: add them to "
|
|
956
|
+
"each environment's env file (the dev chart runs its bundled Postgres"
|
|
957
|
+
+ (" and Redis)" if current.runtime == "langgraph-server" else ")")
|
|
958
|
+
)
|
|
959
|
+
if (keys_added or keys_removed) and current.deployment_target == "kubernetes":
|
|
960
|
+
change = ", ".join([*(f"+{k}" for k in keys_added), *(f"-{k}" for k in keys_removed)])
|
|
961
|
+
items.append(
|
|
962
|
+
f"secrets.keys changed ({change}): run `graph-agents-cli secrets apply --env <env>` "
|
|
963
|
+
"for every environment you deploy to, so each Secret carries the new keys"
|
|
964
|
+
)
|
|
965
|
+
return items
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
_ABSENT = object()
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _values_lookup(data: Any, path: tuple[str, ...]) -> Any:
|
|
972
|
+
for key in path:
|
|
973
|
+
if not isinstance(data, dict) or key not in data:
|
|
974
|
+
return _ABSENT
|
|
975
|
+
data = data[key]
|
|
976
|
+
return data
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _chart_followups(
|
|
980
|
+
project_dir: pathlib.Path, previous: CreateParams, current: CreateParams
|
|
981
|
+
) -> list[Followup]:
|
|
982
|
+
"""Chart values that still disagree with a new runtime or provider (required items).
|
|
983
|
+
|
|
984
|
+
Whatever the merge could or could not apply, this reads the chart values as
|
|
985
|
+
Helm will (values.yaml, overlaid by each values-<env>.yaml; a null removes
|
|
986
|
+
a key) and names every file that still sets a key the new settings decide:
|
|
987
|
+
``runtime`` (the chart wires DATABASE_URI/REDIS_URI or POSTGRES_DSN and
|
|
988
|
+
LANGGRAPH_SERVER from it), ``env.CHECKPOINTER`` under fastapi, and
|
|
989
|
+
``env.MODEL_PROVIDER`` / ``env.MODEL_NAME`` / ``env.OPENAI_BASE_URL`` after a
|
|
990
|
+
provider change. A deploy with any of them left would run the old settings.
|
|
991
|
+
"""
|
|
992
|
+
import yaml
|
|
993
|
+
|
|
994
|
+
if current.deployment_target != "kubernetes":
|
|
995
|
+
return []
|
|
996
|
+
checks: list[_ChartCheck] = []
|
|
997
|
+
if previous.runtime != current.runtime:
|
|
998
|
+
checks.append(
|
|
999
|
+
_ChartCheck(
|
|
1000
|
+
("runtime",),
|
|
1001
|
+
current.runtime,
|
|
1002
|
+
f"set it to {current.runtime!r}",
|
|
1003
|
+
"the chart wires the database variables and LANGGRAPH_SERVER from it",
|
|
1004
|
+
# The chart reads an absent runtime as fastapi.
|
|
1005
|
+
absent_ok=current.runtime == "fastapi",
|
|
1006
|
+
)
|
|
1007
|
+
)
|
|
1008
|
+
if current.runtime == "fastapi" and current.checkpointer == "postgres":
|
|
1009
|
+
checks.append(
|
|
1010
|
+
_ChartCheck(
|
|
1011
|
+
("env", "CHECKPOINTER"),
|
|
1012
|
+
"postgres",
|
|
1013
|
+
"set it to 'postgres'",
|
|
1014
|
+
"without it the fastapi runtime keeps threads in memory",
|
|
1015
|
+
)
|
|
1016
|
+
)
|
|
1017
|
+
if previous.model_provider != current.model_provider:
|
|
1018
|
+
# Absent from the chart: set elsewhere (the Secret, extra env), not ours to judge.
|
|
1019
|
+
checks.append(
|
|
1020
|
+
_ChartCheck(
|
|
1021
|
+
("env", "MODEL_PROVIDER"),
|
|
1022
|
+
current.model_provider,
|
|
1023
|
+
f"set it to {current.model_provider!r}",
|
|
1024
|
+
"the provider the pod calls",
|
|
1025
|
+
absent_ok=True,
|
|
1026
|
+
)
|
|
1027
|
+
)
|
|
1028
|
+
checks.append(
|
|
1029
|
+
_ChartCheck(
|
|
1030
|
+
("env", "MODEL_NAME"),
|
|
1031
|
+
current.model,
|
|
1032
|
+
f"set it to {current.model!r} or another {current.model_provider} model",
|
|
1033
|
+
f"a model chosen for {previous.model_provider} does not run on "
|
|
1034
|
+
f"{current.model_provider}",
|
|
1035
|
+
absent_ok=True,
|
|
1036
|
+
)
|
|
1037
|
+
)
|
|
1038
|
+
if current.model_provider == "openai-compatible":
|
|
1039
|
+
checks.append(
|
|
1040
|
+
_ChartCheck(
|
|
1041
|
+
("env", "OPENAI_BASE_URL"),
|
|
1042
|
+
None,
|
|
1043
|
+
"set it to the endpoint's base URL",
|
|
1044
|
+
"openai-compatible has no default endpoint",
|
|
1045
|
+
)
|
|
1046
|
+
)
|
|
1047
|
+
config = find_project_config(project_dir)
|
|
1048
|
+
secret_keys = set(config.secret_keys) if config else set()
|
|
1049
|
+
# A variable the Secret carries does not have to be in the chart values.
|
|
1050
|
+
checks = [
|
|
1051
|
+
check
|
|
1052
|
+
for check in checks
|
|
1053
|
+
if not (check.path[0] == "env" and check.path[-1] in secret_keys and check.wanted is None)
|
|
1054
|
+
]
|
|
1055
|
+
if not checks:
|
|
1056
|
+
return []
|
|
1057
|
+
|
|
1058
|
+
items: list[Followup] = []
|
|
1059
|
+
for values in sorted((project_dir / "deployment" / "helm").glob("*/values.yaml")):
|
|
1060
|
+
layers: list[tuple[pathlib.Path, Any]] = []
|
|
1061
|
+
for path in [values, *sorted(values.parent.glob("values-*.yaml"))]:
|
|
1062
|
+
try:
|
|
1063
|
+
layers.append((path, yaml.safe_load(path.read_text(encoding="utf-8")) or {}))
|
|
1064
|
+
except (OSError, UnicodeDecodeError, yaml.YAMLError) as exc:
|
|
1065
|
+
items.append(
|
|
1066
|
+
Followup(
|
|
1067
|
+
f"{path.relative_to(project_dir).as_posix()}: cannot be read ({exc}); "
|
|
1068
|
+
"check it by hand against the new settings",
|
|
1069
|
+
required=True,
|
|
1070
|
+
)
|
|
1071
|
+
)
|
|
1072
|
+
if not layers or layers[0][0] != values:
|
|
1073
|
+
continue
|
|
1074
|
+
# values.yaml alone, then values.yaml under each environment's file.
|
|
1075
|
+
stacks = [layers[:1], *([layers[0], layer] for layer in layers[1:])]
|
|
1076
|
+
reported: set[tuple[pathlib.Path, tuple[str, ...]]] = set()
|
|
1077
|
+
for check in checks:
|
|
1078
|
+
for stack in stacks:
|
|
1079
|
+
actual, source = _ABSENT, values
|
|
1080
|
+
for path, data in stack:
|
|
1081
|
+
found = _values_lookup(data, check.path)
|
|
1082
|
+
if found is not _ABSENT:
|
|
1083
|
+
actual, source = (_ABSENT if found is None else found), path
|
|
1084
|
+
if actual is _ABSENT:
|
|
1085
|
+
ok = check.absent_ok
|
|
1086
|
+
elif check.wanted is None:
|
|
1087
|
+
ok = str(actual).strip() != ""
|
|
1088
|
+
else:
|
|
1089
|
+
ok = str(actual) == str(check.wanted)
|
|
1090
|
+
if ok or (source, check.path) in reported:
|
|
1091
|
+
continue
|
|
1092
|
+
reported.add((source, check.path))
|
|
1093
|
+
where = source.relative_to(project_dir).as_posix()
|
|
1094
|
+
dotted = ".".join(check.path)
|
|
1095
|
+
state = "is not set" if actual is _ABSENT else f"is {actual!r}"
|
|
1096
|
+
items.append(
|
|
1097
|
+
Followup(
|
|
1098
|
+
f"{where}: {dotted} {state}: {check.todo} ({check.why})", required=True
|
|
1099
|
+
)
|
|
1100
|
+
)
|
|
1101
|
+
return items
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
@dataclasses.dataclass(frozen=True)
|
|
1105
|
+
class _ChartCheck:
|
|
1106
|
+
"""A chart value the new settings decide (``wanted`` None: any non-empty value)."""
|
|
1107
|
+
|
|
1108
|
+
path: tuple[str, ...]
|
|
1109
|
+
wanted: str | None
|
|
1110
|
+
todo: str
|
|
1111
|
+
why: str
|
|
1112
|
+
absent_ok: bool = False
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
@click.command()
|
|
1116
|
+
@click.pass_context
|
|
1117
|
+
@click.argument(
|
|
1118
|
+
"template_path",
|
|
1119
|
+
type=click.Path(path_type=pathlib.Path),
|
|
1120
|
+
default=".",
|
|
1121
|
+
required=False,
|
|
1122
|
+
)
|
|
1123
|
+
@click.option(
|
|
1124
|
+
"--name",
|
|
1125
|
+
"-n",
|
|
1126
|
+
help=(
|
|
1127
|
+
"Project name for templating (default: the manifest's name, else the current "
|
|
1128
|
+
"directory name)"
|
|
1129
|
+
),
|
|
1130
|
+
)
|
|
1131
|
+
@shared_template_options
|
|
1132
|
+
@click.option(
|
|
1133
|
+
"--force",
|
|
1134
|
+
is_flag=True,
|
|
1135
|
+
help="Force overwrite all files (skip smart-merge comparison)",
|
|
1136
|
+
default=False,
|
|
1137
|
+
)
|
|
1138
|
+
@click.option(
|
|
1139
|
+
"--dry-run",
|
|
1140
|
+
"--dryrun",
|
|
1141
|
+
is_flag=True,
|
|
1142
|
+
help="Preview changes without applying them (requires saved metadata)",
|
|
1143
|
+
default=False,
|
|
1144
|
+
)
|
|
1145
|
+
@click.option(
|
|
1146
|
+
"--prefer-new",
|
|
1147
|
+
is_flag=True,
|
|
1148
|
+
help="Resolve conflicts in favor of the new template version",
|
|
1149
|
+
default=False,
|
|
1150
|
+
)
|
|
1151
|
+
@click.option(
|
|
1152
|
+
"--skip-welcome",
|
|
1153
|
+
is_flag=True,
|
|
1154
|
+
hidden=True,
|
|
1155
|
+
help="Skip the welcome banner (used by nested commands)",
|
|
1156
|
+
default=False,
|
|
1157
|
+
)
|
|
1158
|
+
def enhance(
|
|
1159
|
+
ctx: click.Context,
|
|
1160
|
+
template_path: pathlib.Path,
|
|
1161
|
+
*,
|
|
1162
|
+
name: str | None,
|
|
1163
|
+
runtime: str | None,
|
|
1164
|
+
model_provider: str | None,
|
|
1165
|
+
model: str | None,
|
|
1166
|
+
checkpointer: str | None,
|
|
1167
|
+
deployment_target: str | None,
|
|
1168
|
+
registry: str | None,
|
|
1169
|
+
cd: str | None,
|
|
1170
|
+
auth_policy: str | None,
|
|
1171
|
+
api_policy: str | None,
|
|
1172
|
+
process: str | None,
|
|
1173
|
+
prototype: bool,
|
|
1174
|
+
agent_directory: str | None,
|
|
1175
|
+
agent_guidance_filename: str,
|
|
1176
|
+
base_template: str | None,
|
|
1177
|
+
interactive: bool,
|
|
1178
|
+
auto_approve: bool,
|
|
1179
|
+
skip_checks: bool,
|
|
1180
|
+
skip_deps: bool,
|
|
1181
|
+
debug: bool,
|
|
1182
|
+
force: bool,
|
|
1183
|
+
dry_run: bool,
|
|
1184
|
+
prefer_new: bool,
|
|
1185
|
+
skip_welcome: bool = False,
|
|
1186
|
+
) -> None:
|
|
1187
|
+
"""Add or change the deployment target, CD mode, or runtime of an existing project.
|
|
1188
|
+
|
|
1189
|
+
Applies a template in-place, adding infrastructure files without touching
|
|
1190
|
+
your agent logic. It always enhances the current directory.
|
|
1191
|
+
|
|
1192
|
+
TEMPLATE_PATH says which template to apply, not which project to enhance.
|
|
1193
|
+
It defaults to the current directory, which re-renders the scaffolding from
|
|
1194
|
+
the base template and puts your files back on top.
|
|
1195
|
+
|
|
1196
|
+
If the project has a graph-agents-cli-manifest.yaml, the template recorded
|
|
1197
|
+
there is re-applied and TEMPLATE_PATH is ignored. Pass a local directory or
|
|
1198
|
+
a remote spec (org/repo@tag) for a project graph-agents-cli did not create.
|
|
1199
|
+
|
|
1200
|
+
--base-template is separate. It names a base template this CLI ships, which
|
|
1201
|
+
sits underneath whatever TEMPLATE_PATH supplies.
|
|
1202
|
+
|
|
1203
|
+
api-policy.yaml is never touched by enhance: it belongs to the project;
|
|
1204
|
+
change it with `graph-agents-cli api`.
|
|
1205
|
+
|
|
1206
|
+
A runtime or model-provider change is applied to the files it shapes,
|
|
1207
|
+
including ones you edited (the chart values key by key, around your
|
|
1208
|
+
edits). What cannot be applied is listed under 'Left for you'; the steps
|
|
1209
|
+
marked (required) leave the image or the chart on the old settings (an
|
|
1210
|
+
edited Dockerfile gets the new version beside it as Dockerfile.new).
|
|
1211
|
+
|
|
1212
|
+
Use --dry-run to preview changes before applying them.
|
|
1213
|
+
|
|
1214
|
+
\b
|
|
1215
|
+
Exit codes:
|
|
1216
|
+
0 applied (anything left for you is optional)
|
|
1217
|
+
1 applied, but steps marked (required) are left for you
|
|
1218
|
+
2 usage error (e.g. a model chosen for another provider)
|
|
1219
|
+
3 configuration error (no project for --dry-run, a legacy policy file)
|
|
1220
|
+
"""
|
|
1221
|
+
if not skip_welcome:
|
|
1222
|
+
display_welcome_banner(enhance_mode=True, quiet=auto_approve)
|
|
1223
|
+
|
|
1224
|
+
if debug:
|
|
1225
|
+
logging.basicConfig(level=logging.DEBUG, force=True)
|
|
1226
|
+
console.print("> Debug mode enabled")
|
|
1227
|
+
logging.debug("Starting enhance command in debug mode")
|
|
1228
|
+
|
|
1229
|
+
if api_policy:
|
|
1230
|
+
raise click.UsageError(
|
|
1231
|
+
"--api-policy is not accepted by enhance: api-policy.yaml belongs to the "
|
|
1232
|
+
"project, and enhance never touches it. Change it with `graph-agents-cli api` "
|
|
1233
|
+
"(add, access, allow, deny, revoke, limits, remove), which also keeps the "
|
|
1234
|
+
"manifest, .env.example and the chart values in step."
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
current_dir = pathlib.Path.cwd()
|
|
1238
|
+
_api_policy.ensure_no_legacy_api_policy(find_project_root(current_dir) or current_dir)
|
|
1239
|
+
|
|
1240
|
+
cli_override_args: dict[str, Any] = {}
|
|
1241
|
+
for key, value in (
|
|
1242
|
+
("deployment_target", deployment_target),
|
|
1243
|
+
("runtime", runtime),
|
|
1244
|
+
("model_provider", model_provider),
|
|
1245
|
+
("model", model),
|
|
1246
|
+
("checkpointer", checkpointer),
|
|
1247
|
+
("registry", registry),
|
|
1248
|
+
("cd", cd),
|
|
1249
|
+
("auth_policy", auth_policy),
|
|
1250
|
+
("base_template", base_template),
|
|
1251
|
+
("agent_directory", agent_directory),
|
|
1252
|
+
("process", process),
|
|
1253
|
+
):
|
|
1254
|
+
if value:
|
|
1255
|
+
cli_override_args[key] = value
|
|
1256
|
+
if prototype:
|
|
1257
|
+
cli_override_args["prototype"] = True
|
|
1258
|
+
if agent_guidance_filename != DEFAULT_AGENT_GUIDANCE_FILENAME:
|
|
1259
|
+
cli_override_args["agent_guidance_filename"] = agent_guidance_filename
|
|
1260
|
+
|
|
1261
|
+
is_saved_config_subprocess = os.environ.get(_ENV_USING_SAVED_CONFIG) == "1"
|
|
1262
|
+
has_cli_overrides = bool(cli_override_args)
|
|
1263
|
+
|
|
1264
|
+
if dry_run and force:
|
|
1265
|
+
raise click.UsageError("--dry-run is not compatible with --force.")
|
|
1266
|
+
|
|
1267
|
+
if base_template and not validate_base_template(base_template):
|
|
1268
|
+
hint = (
|
|
1269
|
+
f" To apply a fetched template, pass it positionally: graph-agents-cli scaffold enhance {base_template}"
|
|
1270
|
+
if remote_template.is_template_spec(base_template)
|
|
1271
|
+
else f" Available: {', '.join(get_available_base_templates())}"
|
|
1272
|
+
)
|
|
1273
|
+
raise click.ClickException(
|
|
1274
|
+
f"--base-template takes a template this CLI ships, not '{base_template}'.\n{hint}"
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
if not force and not is_saved_config_subprocess:
|
|
1278
|
+
project_config = find_project_config(current_dir)
|
|
1279
|
+
if project_config:
|
|
1280
|
+
overrides: dict[str, Any] | None = None
|
|
1281
|
+
if has_cli_overrides:
|
|
1282
|
+
overrides = cli_override_args
|
|
1283
|
+
elif interactive:
|
|
1284
|
+
saved_config_result = check_and_execute_with_saved_config(
|
|
1285
|
+
project_dir=current_dir,
|
|
1286
|
+
auto_approve=auto_approve,
|
|
1287
|
+
cli_overrides=cli_override_args,
|
|
1288
|
+
dry_run=dry_run,
|
|
1289
|
+
interactive=interactive,
|
|
1290
|
+
)
|
|
1291
|
+
if saved_config_result is True:
|
|
1292
|
+
return
|
|
1293
|
+
elif isinstance(saved_config_result, dict):
|
|
1294
|
+
overrides = saved_config_result
|
|
1295
|
+
else:
|
|
1296
|
+
if dry_run:
|
|
1297
|
+
raise click.UsageError(
|
|
1298
|
+
"--dry-run requires specifying what to change (e.g. "
|
|
1299
|
+
"--deployment-target kubernetes or --cd argocd) or --interactive."
|
|
1300
|
+
)
|
|
1301
|
+
if check_and_execute_with_saved_config(
|
|
1302
|
+
project_dir=current_dir,
|
|
1303
|
+
auto_approve=auto_approve,
|
|
1304
|
+
cli_overrides=cli_override_args,
|
|
1305
|
+
interactive=interactive,
|
|
1306
|
+
):
|
|
1307
|
+
return
|
|
1308
|
+
|
|
1309
|
+
effective_overrides = overrides if overrides else None
|
|
1310
|
+
if _run_smart_merge(
|
|
1311
|
+
project_dir=current_dir,
|
|
1312
|
+
project_config=project_config,
|
|
1313
|
+
cli_overrides=effective_overrides,
|
|
1314
|
+
auto_approve=auto_approve,
|
|
1315
|
+
dry_run=dry_run,
|
|
1316
|
+
prefer_new=prefer_new,
|
|
1317
|
+
interactive=interactive,
|
|
1318
|
+
):
|
|
1319
|
+
return
|
|
1320
|
+
console.print("[yellow]⚠️ Smart-merge failed, falling back to standard mode.[/yellow]")
|
|
1321
|
+
elif dry_run:
|
|
1322
|
+
raise NotInProjectError(
|
|
1323
|
+
"--dry-run compares against the project's saved settings, and there is no "
|
|
1324
|
+
"graph-agents-cli-manifest.yaml here or in a parent directory.\n"
|
|
1325
|
+
" Run it from a project created by graph-agents-cli."
|
|
1326
|
+
)
|
|
1327
|
+
elif has_cli_overrides:
|
|
1328
|
+
console.print("[dim]No saved metadata found - using standard overwrite mode.[/dim]")
|
|
1329
|
+
else:
|
|
1330
|
+
if not is_saved_config_subprocess:
|
|
1331
|
+
saved_config_result = check_and_execute_with_saved_config(
|
|
1332
|
+
project_dir=current_dir,
|
|
1333
|
+
auto_approve=auto_approve,
|
|
1334
|
+
cli_overrides=cli_override_args,
|
|
1335
|
+
force=force,
|
|
1336
|
+
interactive=interactive,
|
|
1337
|
+
)
|
|
1338
|
+
if saved_config_result is True:
|
|
1339
|
+
return
|
|
1340
|
+
elif not force:
|
|
1341
|
+
project_config = find_project_config(current_dir)
|
|
1342
|
+
if project_config:
|
|
1343
|
+
if _run_smart_merge(
|
|
1344
|
+
project_dir=current_dir,
|
|
1345
|
+
project_config=project_config,
|
|
1346
|
+
cli_overrides=None,
|
|
1347
|
+
auto_approve=auto_approve,
|
|
1348
|
+
dry_run=False,
|
|
1349
|
+
prefer_new=prefer_new,
|
|
1350
|
+
interactive=interactive,
|
|
1351
|
+
):
|
|
1352
|
+
return
|
|
1353
|
+
|
|
1354
|
+
# ---- Standard (overwrite) mode: render in-folder through `create` ----
|
|
1355
|
+
|
|
1356
|
+
if not interactive and not cd and not prototype:
|
|
1357
|
+
if auto_approve and deployment_target == "kubernetes":
|
|
1358
|
+
console.print(
|
|
1359
|
+
"[yellow]Warning: --cd not specified with --auto-approve. "
|
|
1360
|
+
"Keeping the recorded value (or 'skip'). Use --cd to configure CD.[/yellow]"
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
recorded_config = find_project_config(current_dir)
|
|
1364
|
+
if name:
|
|
1365
|
+
project_name = name
|
|
1366
|
+
elif recorded_config is not None and recorded_config.project_name:
|
|
1367
|
+
# The name the project was created with (its chart directory, release
|
|
1368
|
+
# and image are named after it), whatever the checkout directory is called.
|
|
1369
|
+
project_name = recorded_config.project_name
|
|
1370
|
+
console.print(
|
|
1371
|
+
f"Using the project name recorded in graph-agents-cli-manifest.yaml: {project_name}",
|
|
1372
|
+
style="dim",
|
|
1373
|
+
)
|
|
1374
|
+
else:
|
|
1375
|
+
project_name = current_dir.name
|
|
1376
|
+
console.print(f"Using current directory name as project name: {project_name}", style="dim")
|
|
1377
|
+
|
|
1378
|
+
if interactive:
|
|
1379
|
+
console.print()
|
|
1380
|
+
console.print("🚀 [blue]Ready to enhance your project with deployment capabilities[/blue]")
|
|
1381
|
+
console.print(f"📂 {current_dir}")
|
|
1382
|
+
console.print()
|
|
1383
|
+
console.print("[bold]What will happen:[/bold]")
|
|
1384
|
+
console.print("• New template files will be added to this directory")
|
|
1385
|
+
console.print("• Your existing files will be preserved")
|
|
1386
|
+
console.print("• A backup will be created in ~/.graph-agents-cli/backups/")
|
|
1387
|
+
console.print()
|
|
1388
|
+
|
|
1389
|
+
if not click.confirm(
|
|
1390
|
+
f"Continue with enhancement? {click.style('[Y/n]: ', fg='blue', bold=True)}",
|
|
1391
|
+
default=True,
|
|
1392
|
+
show_default=False,
|
|
1393
|
+
):
|
|
1394
|
+
console.print("✋ [yellow]Enhancement cancelled.[/yellow]")
|
|
1395
|
+
return
|
|
1396
|
+
console.print()
|
|
1397
|
+
|
|
1398
|
+
if template_path == pathlib.Path("."):
|
|
1399
|
+
agent_spec = "local@."
|
|
1400
|
+
elif template_path.is_dir():
|
|
1401
|
+
agent_spec = f"local@{template_path.resolve()}"
|
|
1402
|
+
else:
|
|
1403
|
+
agent_spec = str(template_path)
|
|
1404
|
+
|
|
1405
|
+
final_agent_directory: str | None = agent_directory
|
|
1406
|
+
|
|
1407
|
+
if agent_spec.startswith("local@"):
|
|
1408
|
+
from ..utils.remote_template import get_base_template_name, load_remote_template_config
|
|
1409
|
+
|
|
1410
|
+
cli_overrides: dict[str, Any] = {}
|
|
1411
|
+
if base_template:
|
|
1412
|
+
cli_overrides["base_template"] = base_template
|
|
1413
|
+
if agent_directory:
|
|
1414
|
+
cli_overrides["settings"] = {"agent_directory": agent_directory}
|
|
1415
|
+
|
|
1416
|
+
source_config = load_remote_template_config(current_dir, cli_overrides)
|
|
1417
|
+
original_base_template_name = get_base_template_name(source_config)
|
|
1418
|
+
|
|
1419
|
+
if not base_template and interactive:
|
|
1420
|
+
selected_base_template = display_base_template_selection(original_base_template_name)
|
|
1421
|
+
base_template = selected_base_template
|
|
1422
|
+
if selected_base_template != original_base_template_name:
|
|
1423
|
+
cli_overrides["base_template"] = selected_base_template
|
|
1424
|
+
console.print(f"✅ Selected base template: [cyan]{selected_base_template}[/cyan]")
|
|
1425
|
+
console.print()
|
|
1426
|
+
elif not base_template and not remote_template.is_template_spec(
|
|
1427
|
+
original_base_template_name
|
|
1428
|
+
):
|
|
1429
|
+
# A project made from a fetched template records that template's
|
|
1430
|
+
# spec here, which names the source, not a base layer this CLI ships.
|
|
1431
|
+
base_template = original_base_template_name
|
|
1432
|
+
|
|
1433
|
+
if cli_overrides.get("base_template"):
|
|
1434
|
+
source_config = load_remote_template_config(current_dir, cli_overrides)
|
|
1435
|
+
|
|
1436
|
+
base_template_name = get_base_template_name(source_config)
|
|
1437
|
+
if interactive or base_template:
|
|
1438
|
+
console.print()
|
|
1439
|
+
console.print(f"Template inherits from base: [cyan]{base_template_name}[/cyan]")
|
|
1440
|
+
console.print()
|
|
1441
|
+
|
|
1442
|
+
if template_path == pathlib.Path("."):
|
|
1443
|
+
gacli_config = find_project_config(current_dir)
|
|
1444
|
+
if not gacli_config:
|
|
1445
|
+
console.print(
|
|
1446
|
+
"[dim]No saved metadata found - defaulting to Python as project language.[/dim]"
|
|
1447
|
+
)
|
|
1448
|
+
|
|
1449
|
+
detected_agent_directory = "app"
|
|
1450
|
+
if not agent_directory and gacli_config and gacli_config.agent_directory:
|
|
1451
|
+
detected_agent_directory = gacli_config.agent_directory
|
|
1452
|
+
|
|
1453
|
+
if not agent_directory and interactive:
|
|
1454
|
+
final_agent_directory = display_agent_directory_selection(
|
|
1455
|
+
current_dir, detected_agent_directory
|
|
1456
|
+
)
|
|
1457
|
+
console.print(f"✅ Selected agent directory: [cyan]{final_agent_directory}[/cyan]")
|
|
1458
|
+
console.print()
|
|
1459
|
+
else:
|
|
1460
|
+
final_agent_directory = agent_directory or detected_agent_directory
|
|
1461
|
+
|
|
1462
|
+
if agent_directory:
|
|
1463
|
+
console.print(
|
|
1464
|
+
f"Info: Using CLI-specified agent directory: [cyan]{agent_directory}[/cyan]"
|
|
1465
|
+
)
|
|
1466
|
+
elif detected_agent_directory != "app":
|
|
1467
|
+
console.print(
|
|
1468
|
+
f"Info: Auto-detected agent directory: [cyan]{detected_agent_directory}[/cyan]"
|
|
1469
|
+
)
|
|
1470
|
+
|
|
1471
|
+
agent_folder = current_dir / final_agent_directory
|
|
1472
|
+
if not agent_folder.is_dir():
|
|
1473
|
+
console.print()
|
|
1474
|
+
console.print("⚠️ [bold yellow]PROJECT STRUCTURE WARNING[/bold yellow] ⚠️")
|
|
1475
|
+
console.print(
|
|
1476
|
+
f"📁 Expected [cyan]/{final_agent_directory}[/cyan] folder containing your agent code; "
|
|
1477
|
+
f"it is missing from {current_dir}."
|
|
1478
|
+
)
|
|
1479
|
+
console.print(
|
|
1480
|
+
" Create it and move your agent code there, or use "
|
|
1481
|
+
"[cyan]--agent-directory <name>[/cyan] to name your existing directory."
|
|
1482
|
+
)
|
|
1483
|
+
console.print()
|
|
1484
|
+
if interactive and not click.confirm(
|
|
1485
|
+
f"Continue with enhancement despite missing /{final_agent_directory} folder?",
|
|
1486
|
+
default=True,
|
|
1487
|
+
):
|
|
1488
|
+
console.print("✋ [yellow]Enhancement cancelled.[/yellow]")
|
|
1489
|
+
return
|
|
1490
|
+
else:
|
|
1491
|
+
required_var = get_language_config("python")["agent_variable"]
|
|
1492
|
+
agent_file = find_agent_file(current_dir, final_agent_directory)
|
|
1493
|
+
if agent_file:
|
|
1494
|
+
console.print(f"✅ Found [cyan]{agent_file.relative_to(current_dir)}[/cyan]")
|
|
1495
|
+
is_valid, error_msg = validate_agent_file(agent_file)
|
|
1496
|
+
if is_valid:
|
|
1497
|
+
console.print(f"✅ Found '{required_var}' definition in {agent_file.name}")
|
|
1498
|
+
else:
|
|
1499
|
+
console.print(f"⚠️ [yellow]{error_msg}[/yellow]")
|
|
1500
|
+
console.print(
|
|
1501
|
+
f" app/agent.py must export the compiled graph as [cyan]{required_var}[/cyan]."
|
|
1502
|
+
)
|
|
1503
|
+
if interactive and not click.confirm(
|
|
1504
|
+
f"Continue enhancement? (You can add '{required_var}' later)", default=True
|
|
1505
|
+
):
|
|
1506
|
+
console.print("✋ [yellow]Enhancement cancelled.[/yellow]")
|
|
1507
|
+
return
|
|
1508
|
+
else:
|
|
1509
|
+
console.print(
|
|
1510
|
+
f"⚠️ [yellow]Warning: agent.py not found in {final_agent_directory}/[/yellow]"
|
|
1511
|
+
)
|
|
1512
|
+
console.print(
|
|
1513
|
+
f" Create {final_agent_directory}/agent.py exporting [cyan]{required_var}[/cyan]"
|
|
1514
|
+
)
|
|
1515
|
+
if interactive and not click.confirm(
|
|
1516
|
+
"Continue enhancement? (An example agent.py will be created for you)",
|
|
1517
|
+
default=True,
|
|
1518
|
+
):
|
|
1519
|
+
console.print("✋ [yellow]Enhancement cancelled.[/yellow]")
|
|
1520
|
+
return
|
|
1521
|
+
|
|
1522
|
+
final_cli_overrides: dict[str, Any] = {}
|
|
1523
|
+
if base_template:
|
|
1524
|
+
final_cli_overrides["base_template"] = base_template
|
|
1525
|
+
if template_path == pathlib.Path(".") and final_agent_directory:
|
|
1526
|
+
final_cli_overrides["settings"] = {"agent_directory": final_agent_directory}
|
|
1527
|
+
|
|
1528
|
+
effective_create_params = _backfill_create_params_from_config(
|
|
1529
|
+
current_dir,
|
|
1530
|
+
{
|
|
1531
|
+
"deployment_target": deployment_target,
|
|
1532
|
+
"runtime": runtime,
|
|
1533
|
+
"model_provider": model_provider,
|
|
1534
|
+
"model": model,
|
|
1535
|
+
"checkpointer": checkpointer,
|
|
1536
|
+
"registry": registry,
|
|
1537
|
+
"cd": cd,
|
|
1538
|
+
"auth_policy": auth_policy,
|
|
1539
|
+
},
|
|
1540
|
+
)
|
|
1541
|
+
|
|
1542
|
+
# Read before the render: create rewrites the manifest in place.
|
|
1543
|
+
existing_config = find_project_config(current_dir)
|
|
1544
|
+
previous_params = _recorded_params(existing_config, current_dir) if existing_config else None
|
|
1545
|
+
recorded_base = existing_config.base_template if existing_config else None
|
|
1546
|
+
effective_process = process or (existing_config.process if existing_config else None)
|
|
1547
|
+
# Keep the recorded guidance file unless one was asked for explicitly.
|
|
1548
|
+
effective_guidance = agent_guidance_filename
|
|
1549
|
+
if agent_guidance_filename == DEFAULT_AGENT_GUIDANCE_FILENAME and existing_config:
|
|
1550
|
+
effective_guidance = existing_config.agent_guidance_filename
|
|
1551
|
+
|
|
1552
|
+
ctx.invoke(
|
|
1553
|
+
create,
|
|
1554
|
+
project_name=project_name,
|
|
1555
|
+
agent=agent_spec,
|
|
1556
|
+
output_dir=None,
|
|
1557
|
+
runtime=effective_create_params["runtime"],
|
|
1558
|
+
model_provider=effective_create_params["model_provider"],
|
|
1559
|
+
model=effective_create_params["model"],
|
|
1560
|
+
checkpointer=effective_create_params["checkpointer"],
|
|
1561
|
+
deployment_target=effective_create_params["deployment_target"],
|
|
1562
|
+
registry=effective_create_params["registry"],
|
|
1563
|
+
cd=effective_create_params["cd"],
|
|
1564
|
+
auth_policy=effective_create_params["auth_policy"],
|
|
1565
|
+
api_policy=None,
|
|
1566
|
+
process=effective_process,
|
|
1567
|
+
prototype=prototype,
|
|
1568
|
+
agent_directory=final_agent_directory
|
|
1569
|
+
if template_path == pathlib.Path(".")
|
|
1570
|
+
else agent_directory,
|
|
1571
|
+
agent_guidance_filename=effective_guidance,
|
|
1572
|
+
base_template=base_template,
|
|
1573
|
+
interactive=interactive,
|
|
1574
|
+
auto_approve=auto_approve,
|
|
1575
|
+
skip_checks=skip_checks,
|
|
1576
|
+
skip_deps=skip_deps,
|
|
1577
|
+
debug=debug,
|
|
1578
|
+
skip_welcome=True,
|
|
1579
|
+
in_folder=True,
|
|
1580
|
+
cli_overrides=final_cli_overrides if final_cli_overrides else None,
|
|
1581
|
+
)
|
|
1582
|
+
|
|
1583
|
+
# An in-folder render rewrites the manifest from the template it rendered,
|
|
1584
|
+
# which for a fetched template is a synthesized local name. The project came
|
|
1585
|
+
# from the recorded spec and still has to re-fetch it next time.
|
|
1586
|
+
if recorded_base and remote_template.is_template_spec(recorded_base):
|
|
1587
|
+
update_cli_metadata(current_dir, {}, base_template=recorded_base)
|
|
1588
|
+
|
|
1589
|
+
if existing_config is not None and previous_params is not None:
|
|
1590
|
+
new_config = find_project_config(current_dir)
|
|
1591
|
+
if new_config is not None:
|
|
1592
|
+
_reconcile_after_in_folder_render(
|
|
1593
|
+
project_dir=current_dir,
|
|
1594
|
+
project_name=project_name,
|
|
1595
|
+
existing_config=existing_config,
|
|
1596
|
+
previous=previous_params,
|
|
1597
|
+
new_config=new_config,
|
|
1598
|
+
)
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
# --api-policy is shared with create but refused here (the policy belongs to the
|
|
1602
|
+
# project; `graph-agents-cli api` changes it), so --help does not offer it.
|
|
1603
|
+
for _param in enhance.params:
|
|
1604
|
+
if _param.name == "api_policy" and isinstance(_param, click.Option):
|
|
1605
|
+
_param.hidden = True
|
|
1606
|
+
|
|
1607
|
+
|
|
1608
|
+
def _reconcile_after_in_folder_render(
|
|
1609
|
+
*,
|
|
1610
|
+
project_dir: pathlib.Path,
|
|
1611
|
+
project_name: str,
|
|
1612
|
+
existing_config: ProjectConfig,
|
|
1613
|
+
previous: CreateParams,
|
|
1614
|
+
new_config: ProjectConfig,
|
|
1615
|
+
) -> None:
|
|
1616
|
+
"""Finish a ``--force`` (in-folder) render whose settings changed.
|
|
1617
|
+
|
|
1618
|
+
The in-folder render overlays the project on the new render, so it only
|
|
1619
|
+
adds the files the project lacks: everything it already has (the chart
|
|
1620
|
+
values, ``.env.example``, ``pyproject.toml``'s dependencies, ...) would keep
|
|
1621
|
+
the old settings. The same three-way pass as the smart merge brings every
|
|
1622
|
+
file the developer did not edit to what a fresh ``create`` renders, merges
|
|
1623
|
+
the template's change into edited config files, recomputes ``secrets.keys``
|
|
1624
|
+
and prints what is left, keeping the developer's version of anything else
|
|
1625
|
+
they changed.
|
|
1626
|
+
"""
|
|
1627
|
+
current = _recorded_params(new_config, project_dir)
|
|
1628
|
+
if current == previous:
|
|
1629
|
+
return
|
|
1630
|
+
|
|
1631
|
+
def _after_merge(proj_dir: pathlib.Path, _language: str) -> list[str | Followup]:
|
|
1632
|
+
added, removed = reconcile_secret_keys(proj_dir, previous=previous, current=current)
|
|
1633
|
+
return [
|
|
1634
|
+
*_settings_followups(proj_dir, previous, current, added, removed),
|
|
1635
|
+
*_chart_followups(proj_dir, previous, current),
|
|
1636
|
+
]
|
|
1637
|
+
|
|
1638
|
+
console.print()
|
|
1639
|
+
console.print("Reconciling the files the new settings shape...", style="dim")
|
|
1640
|
+
run_three_way_merge(
|
|
1641
|
+
project_dir=project_dir,
|
|
1642
|
+
project_name=project_name,
|
|
1643
|
+
agent_directory=new_config.agent_directory,
|
|
1644
|
+
language=new_config.language,
|
|
1645
|
+
old_args=metadata_to_cli_args(existing_config),
|
|
1646
|
+
new_args=metadata_to_cli_args(new_config),
|
|
1647
|
+
auto_approve=True,
|
|
1648
|
+
dry_run=False,
|
|
1649
|
+
operation_label="enhancement",
|
|
1650
|
+
post_apply_hook=_after_merge,
|
|
1651
|
+
merge_config=True,
|
|
1652
|
+
)
|