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,1424 @@
|
|
|
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
|
+
"""``create``: scaffold a LangGraph agent project."""
|
|
17
|
+
|
|
18
|
+
import dataclasses
|
|
19
|
+
import functools
|
|
20
|
+
import logging
|
|
21
|
+
import pathlib
|
|
22
|
+
import re
|
|
23
|
+
import shutil
|
|
24
|
+
import tempfile
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
|
|
28
|
+
import click
|
|
29
|
+
from rich.prompt import IntPrompt, Prompt
|
|
30
|
+
|
|
31
|
+
from graph_agents_cli import _api_policy, _response_schema
|
|
32
|
+
from graph_agents_cli._defaults import (
|
|
33
|
+
DEFAULT_AGENT_GUIDANCE_FILENAME,
|
|
34
|
+
DEFAULT_AUTH_POLICY,
|
|
35
|
+
DEFAULT_CD,
|
|
36
|
+
DEFAULT_DEPLOYMENT_TARGET,
|
|
37
|
+
DEFAULT_MODEL_PROVIDER,
|
|
38
|
+
DEFAULT_MODELS,
|
|
39
|
+
DEFAULT_REGISTRY_HOST,
|
|
40
|
+
DEFAULT_REGISTRY_PLACEHOLDER,
|
|
41
|
+
DEFAULT_RUNTIME,
|
|
42
|
+
REGISTRY_FIX_COMMAND,
|
|
43
|
+
REGISTRY_FIX_EFFECT,
|
|
44
|
+
)
|
|
45
|
+
from graph_agents_cli._output import Console
|
|
46
|
+
from graph_agents_cli._project import MANIFEST_FILENAME, read_project_config
|
|
47
|
+
from graph_agents_cli.dev import policy_check
|
|
48
|
+
|
|
49
|
+
from ..utils import build_record, cli_options, openapi_seed, remote_template, template
|
|
50
|
+
from ..utils.fs import standard_ignore_patterns
|
|
51
|
+
from ..utils.logging import display_welcome_banner
|
|
52
|
+
from ..utils.manifest import (
|
|
53
|
+
API_POLICY_FILENAME,
|
|
54
|
+
CreateParams,
|
|
55
|
+
finalize_manifest,
|
|
56
|
+
)
|
|
57
|
+
from ..utils.version import get_current_version
|
|
58
|
+
|
|
59
|
+
__all__ = ["create"]
|
|
60
|
+
|
|
61
|
+
console = Console()
|
|
62
|
+
|
|
63
|
+
DEFAULT_AGENT = "langgraph"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class AgentSelection:
|
|
68
|
+
"""Outcome of resolving which agent/template to use and fetching it."""
|
|
69
|
+
|
|
70
|
+
agent: str | None
|
|
71
|
+
final_agent: str
|
|
72
|
+
template_source_path: pathlib.Path | None
|
|
73
|
+
temp_dir_to_clean: str | None
|
|
74
|
+
remote_spec: remote_template.RemoteTemplateSpec | None
|
|
75
|
+
recorded_spec: str | None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class LoadedTemplateConfig:
|
|
80
|
+
"""Outcome of loading and merging a selected template's config."""
|
|
81
|
+
|
|
82
|
+
config: dict
|
|
83
|
+
template_path: pathlib.Path
|
|
84
|
+
base_template_name: str | None
|
|
85
|
+
deployment_agent_name: str
|
|
86
|
+
remote_config: dict | None
|
|
87
|
+
cli_overrides: dict | None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _handle_create_errors(f: Callable) -> Callable:
|
|
91
|
+
"""Convert create failures to Click's concise CLI errors."""
|
|
92
|
+
|
|
93
|
+
@functools.wraps(f)
|
|
94
|
+
def wrapper(*args, **kwargs):
|
|
95
|
+
try:
|
|
96
|
+
return f(*args, **kwargs)
|
|
97
|
+
except (click.ClickException, click.Abort):
|
|
98
|
+
raise
|
|
99
|
+
except ValueError as e:
|
|
100
|
+
raise click.UsageError(str(e)) from e
|
|
101
|
+
except Exception as e:
|
|
102
|
+
raise click.ClickException(str(e)) from e
|
|
103
|
+
|
|
104
|
+
return wrapper
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@click.command()
|
|
108
|
+
@click.pass_context
|
|
109
|
+
@click.argument("project_name", required=False, default=None)
|
|
110
|
+
@click.option(
|
|
111
|
+
"--agent",
|
|
112
|
+
"-a",
|
|
113
|
+
help=(
|
|
114
|
+
"Template to use: a bundled agent (default: langgraph), a local path "
|
|
115
|
+
"(`local@/path/to/template`), or a remote spec (`org/repo/path@ref`, "
|
|
116
|
+
"`https://github.com/org/repo/tree/main/path`)."
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
@click.option(
|
|
120
|
+
"--output-dir",
|
|
121
|
+
"-o",
|
|
122
|
+
type=click.Path(),
|
|
123
|
+
help="Output directory for the project (default: current directory)",
|
|
124
|
+
)
|
|
125
|
+
@click.option(
|
|
126
|
+
"--skip-welcome",
|
|
127
|
+
is_flag=True,
|
|
128
|
+
hidden=True,
|
|
129
|
+
help="Skip the welcome banner",
|
|
130
|
+
default=False,
|
|
131
|
+
)
|
|
132
|
+
@click.option(
|
|
133
|
+
"--quiet",
|
|
134
|
+
is_flag=True,
|
|
135
|
+
hidden=True,
|
|
136
|
+
help="Internal flag: suppress the banner, the notices and the next steps",
|
|
137
|
+
default=False,
|
|
138
|
+
)
|
|
139
|
+
@click.option(
|
|
140
|
+
"--locked",
|
|
141
|
+
is_flag=True,
|
|
142
|
+
hidden=True,
|
|
143
|
+
help="Internal flag for version-locked remote templates",
|
|
144
|
+
default=False,
|
|
145
|
+
)
|
|
146
|
+
@click.option(
|
|
147
|
+
"--response-schema",
|
|
148
|
+
"response_schema",
|
|
149
|
+
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
|
150
|
+
help=(
|
|
151
|
+
"Structured final answers: seed <agent directory>/response_schema.json (the JSON "
|
|
152
|
+
"Schema of the agent's final answer) from this file; checked before the project "
|
|
153
|
+
"is created"
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
@cli_options.shared_template_options
|
|
157
|
+
@_handle_create_errors
|
|
158
|
+
def create(
|
|
159
|
+
ctx: click.Context,
|
|
160
|
+
project_name: str,
|
|
161
|
+
*,
|
|
162
|
+
agent: str | None,
|
|
163
|
+
output_dir: str | None,
|
|
164
|
+
runtime: str | None,
|
|
165
|
+
model_provider: str | None,
|
|
166
|
+
model: str | None,
|
|
167
|
+
checkpointer: str | None,
|
|
168
|
+
deployment_target: str | None,
|
|
169
|
+
registry: str | None,
|
|
170
|
+
cd: str | None,
|
|
171
|
+
auth_policy: str | None,
|
|
172
|
+
api_policy: str | None,
|
|
173
|
+
process: str | None,
|
|
174
|
+
prototype: bool,
|
|
175
|
+
agent_directory: str | None,
|
|
176
|
+
agent_guidance_filename: str,
|
|
177
|
+
base_template: str | None,
|
|
178
|
+
interactive: bool,
|
|
179
|
+
auto_approve: bool,
|
|
180
|
+
skip_checks: bool,
|
|
181
|
+
skip_deps: bool,
|
|
182
|
+
debug: bool,
|
|
183
|
+
skip_welcome: bool = False,
|
|
184
|
+
quiet: bool = False,
|
|
185
|
+
locked: bool = False,
|
|
186
|
+
in_folder: bool = False,
|
|
187
|
+
cli_overrides: dict | None = None,
|
|
188
|
+
response_schema: str | None = None,
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Create a LangGraph agent project from a template."""
|
|
191
|
+
# A quiet run builds a throwaway tree for the three-way merge behind
|
|
192
|
+
# `scaffold enhance` and `scaffold upgrade`, so nothing it says is addressed
|
|
193
|
+
# to anyone. Warnings and errors still print.
|
|
194
|
+
if quiet:
|
|
195
|
+
skip_welcome = True
|
|
196
|
+
|
|
197
|
+
if not skip_welcome:
|
|
198
|
+
display_welcome_banner(agent=agent, quiet=auto_approve)
|
|
199
|
+
|
|
200
|
+
project_name = _resolve_project_name(
|
|
201
|
+
project_name,
|
|
202
|
+
interactive=interactive,
|
|
203
|
+
auto_approve=auto_approve,
|
|
204
|
+
output_dir=output_dir,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if debug:
|
|
208
|
+
logging.basicConfig(level=logging.DEBUG, force=True)
|
|
209
|
+
console.print("> Debug mode enabled")
|
|
210
|
+
logging.debug("Starting CLI in debug mode")
|
|
211
|
+
|
|
212
|
+
destination_dir = (pathlib.Path(output_dir) if output_dir else pathlib.Path.cwd()).resolve()
|
|
213
|
+
|
|
214
|
+
if in_folder:
|
|
215
|
+
# A re-render of a project still on the retired product API policy
|
|
216
|
+
# would drop the policy silently: stop with the migration steps.
|
|
217
|
+
_api_policy.ensure_no_legacy_api_policy(destination_dir)
|
|
218
|
+
|
|
219
|
+
# Validate the seed policy before anything is rendered, including that every
|
|
220
|
+
# OpenAPI spec it references can be copied into the project (lint reads them).
|
|
221
|
+
api_document = _api_policy.load_policy_document(api_policy) if api_policy else None
|
|
222
|
+
# A response schema the agent could not start with is refused before anything is rendered.
|
|
223
|
+
if response_schema:
|
|
224
|
+
_response_schema.load_schema(response_schema)
|
|
225
|
+
spec_copies = (
|
|
226
|
+
openapi_seed.plan_spec_copies(api_document, api_policy)
|
|
227
|
+
if api_document is not None and api_policy
|
|
228
|
+
else []
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
project_path = _prepare_project_path(
|
|
232
|
+
destination_dir,
|
|
233
|
+
project_name,
|
|
234
|
+
in_folder=in_folder,
|
|
235
|
+
auto_approve=auto_approve,
|
|
236
|
+
interactive=interactive,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if not skip_checks and not quiet:
|
|
240
|
+
_preflight()
|
|
241
|
+
|
|
242
|
+
resolved = _resolve_template(
|
|
243
|
+
agent,
|
|
244
|
+
deployment_target=deployment_target,
|
|
245
|
+
interactive=interactive,
|
|
246
|
+
auto_approve=auto_approve,
|
|
247
|
+
locked=locked,
|
|
248
|
+
project_name=project_name,
|
|
249
|
+
base_template=base_template,
|
|
250
|
+
cli_overrides=cli_overrides,
|
|
251
|
+
)
|
|
252
|
+
if resolved is None:
|
|
253
|
+
# A version-locked template executed a nested command; nothing more to do.
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
selection, loaded = resolved
|
|
257
|
+
agent = selection.agent
|
|
258
|
+
final_agent = selection.final_agent
|
|
259
|
+
template_source_path = selection.template_source_path
|
|
260
|
+
temp_dir_to_clean = selection.temp_dir_to_clean
|
|
261
|
+
remote_spec = selection.remote_spec
|
|
262
|
+
recorded_spec = selection.recorded_spec
|
|
263
|
+
config = loaded.config
|
|
264
|
+
template_path = loaded.template_path
|
|
265
|
+
remote_config = loaded.remote_config
|
|
266
|
+
base_template_name = loaded.base_template_name
|
|
267
|
+
cli_overrides = loaded.cli_overrides
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
params = _resolve_create_params(
|
|
271
|
+
deployment_target=deployment_target,
|
|
272
|
+
runtime=runtime,
|
|
273
|
+
model_provider=model_provider,
|
|
274
|
+
model=model,
|
|
275
|
+
checkpointer=checkpointer,
|
|
276
|
+
registry=registry,
|
|
277
|
+
cd=cd,
|
|
278
|
+
auth_policy=auth_policy,
|
|
279
|
+
api_document=api_document,
|
|
280
|
+
api_policy_dir=pathlib.Path(api_policy).resolve().parent if api_policy else None,
|
|
281
|
+
process=process,
|
|
282
|
+
prototype=prototype,
|
|
283
|
+
interactive=interactive,
|
|
284
|
+
auto_approve=auto_approve,
|
|
285
|
+
quiet=quiet,
|
|
286
|
+
skip_checks=skip_checks,
|
|
287
|
+
deployment_agent_name=loaded.deployment_agent_name,
|
|
288
|
+
remote_config=remote_config,
|
|
289
|
+
git_dir=destination_dir,
|
|
290
|
+
)
|
|
291
|
+
except Exception:
|
|
292
|
+
if temp_dir_to_clean:
|
|
293
|
+
shutil.rmtree(temp_dir_to_clean, ignore_errors=True)
|
|
294
|
+
raise
|
|
295
|
+
logging.debug("Resolved create params: %s", params)
|
|
296
|
+
|
|
297
|
+
# An in-folder render (enhance/upgrade overwrite mode) re-renders the manifest
|
|
298
|
+
# and the API policy from the template before finalize_manifest runs, so
|
|
299
|
+
# the developer's recorded state has to be captured here and re-applied.
|
|
300
|
+
existing_policy: bytes | None = None
|
|
301
|
+
carried_build: object = _NO_MANIFEST
|
|
302
|
+
if in_folder:
|
|
303
|
+
params = _carry_recorded_state(destination_dir, params)
|
|
304
|
+
carried_build = _recorded_build_block(destination_dir)
|
|
305
|
+
policy_path = destination_dir / API_POLICY_FILENAME
|
|
306
|
+
if api_policy is None and policy_path.is_file():
|
|
307
|
+
existing_policy = policy_path.read_bytes()
|
|
308
|
+
apis, example = _read_project_policy(policy_path)
|
|
309
|
+
params = dataclasses.replace(
|
|
310
|
+
params, has_api_policy=True, apis=apis, example_call=example
|
|
311
|
+
)
|
|
312
|
+
logging.debug("Create params after carrying the recorded state: %s", params)
|
|
313
|
+
# The carried APIs never went through _resolve_create_params' check: an
|
|
314
|
+
# enhance that switches the runtime must be refused exactly like create.
|
|
315
|
+
problem = _api_policy.forward_runtime_problem(params.apis, params.runtime)
|
|
316
|
+
if problem:
|
|
317
|
+
if temp_dir_to_clean:
|
|
318
|
+
shutil.rmtree(temp_dir_to_clean, ignore_errors=True)
|
|
319
|
+
raise click.UsageError(f"{problem} (in {API_POLICY_FILENAME})")
|
|
320
|
+
|
|
321
|
+
if not template_source_path:
|
|
322
|
+
template_path = template.get_template_path(final_agent)
|
|
323
|
+
logging.debug("Template path: %s", template_path)
|
|
324
|
+
|
|
325
|
+
destination_dir.mkdir(parents=True, exist_ok=True)
|
|
326
|
+
|
|
327
|
+
final_cli_overrides = dict(cli_overrides or {})
|
|
328
|
+
if agent_directory:
|
|
329
|
+
final_cli_overrides.setdefault("settings", {})
|
|
330
|
+
final_cli_overrides["settings"]["agent_directory"] = agent_directory
|
|
331
|
+
|
|
332
|
+
# `local@.` overlays the current directory onto itself, so the manifest in
|
|
333
|
+
# the overlay is this project's own and has to survive the copy. Every other
|
|
334
|
+
# source is a template, whose manifest describes the template.
|
|
335
|
+
overlay_is_project = isinstance(agent, str) and agent.strip().rstrip("/") == "local@."
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
rendered_path = template.process_template(
|
|
339
|
+
agent_name=final_agent,
|
|
340
|
+
template_dir=template_path,
|
|
341
|
+
project_name=project_name,
|
|
342
|
+
deployment_target=params.deployment_target,
|
|
343
|
+
runtime=params.runtime,
|
|
344
|
+
model_provider=params.model_provider,
|
|
345
|
+
model=params.model,
|
|
346
|
+
checkpointer=params.checkpointer,
|
|
347
|
+
registry=params.registry,
|
|
348
|
+
cd=params.cd,
|
|
349
|
+
auth_policy=params.auth_policy,
|
|
350
|
+
has_api_policy=params.has_api_policy,
|
|
351
|
+
apis=params.apis,
|
|
352
|
+
example_call=params.example_call,
|
|
353
|
+
process=params.process,
|
|
354
|
+
output_dir=destination_dir,
|
|
355
|
+
remote_template_path=template_source_path,
|
|
356
|
+
remote_config=config,
|
|
357
|
+
in_folder=in_folder,
|
|
358
|
+
overlay_is_project=overlay_is_project,
|
|
359
|
+
cli_overrides=final_cli_overrides or None,
|
|
360
|
+
remote_spec=remote_spec,
|
|
361
|
+
# The project records the spec it was fetched from, so enhance and
|
|
362
|
+
# upgrade can fetch it again.
|
|
363
|
+
recorded_base_template=recorded_spec if not in_folder else None,
|
|
364
|
+
agent_guidance_filename=agent_guidance_filename,
|
|
365
|
+
auth_policy_implemented=params.auth_policy_implemented,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
spec_lines: list[str] = []
|
|
369
|
+
if api_policy:
|
|
370
|
+
shutil.copy2(api_policy, rendered_path / API_POLICY_FILENAME)
|
|
371
|
+
logging.debug("Seeded %s from %s", API_POLICY_FILENAME, api_policy)
|
|
372
|
+
spec_lines = openapi_seed.install_spec_copies(rendered_path, spec_copies)
|
|
373
|
+
elif existing_policy is not None:
|
|
374
|
+
# api-policy.yaml belongs to the project (it is the security
|
|
375
|
+
# boundary the team reviews): an in-folder re-render must never
|
|
376
|
+
# replace it with the template's example.
|
|
377
|
+
(rendered_path / API_POLICY_FILENAME).write_bytes(existing_policy)
|
|
378
|
+
logging.debug("Restored the project's own %s", API_POLICY_FILENAME)
|
|
379
|
+
if response_schema:
|
|
380
|
+
seeded = _response_schema.schema_file(
|
|
381
|
+
rendered_path, agent_directory or _rendered_agent_directory(rendered_path)
|
|
382
|
+
)
|
|
383
|
+
shutil.copy2(response_schema, seeded)
|
|
384
|
+
logging.debug("Seeded %s from %s", seeded, response_schema)
|
|
385
|
+
|
|
386
|
+
finalize_manifest(
|
|
387
|
+
rendered_path,
|
|
388
|
+
project_name=project_name,
|
|
389
|
+
params=params,
|
|
390
|
+
cli_version=get_current_version(),
|
|
391
|
+
)
|
|
392
|
+
_record_build(
|
|
393
|
+
rendered_path,
|
|
394
|
+
carried=carried_build,
|
|
395
|
+
# The digest describes the snapshot `scaffold upgrade` renders from the
|
|
396
|
+
# manifest's settings, which is this render only when nothing else
|
|
397
|
+
# (a seed policy, a local or remote template) went into it.
|
|
398
|
+
render_is_snapshot=(
|
|
399
|
+
not in_folder
|
|
400
|
+
and api_policy is None
|
|
401
|
+
and response_schema is None
|
|
402
|
+
and template_source_path is None
|
|
403
|
+
and remote_config is None
|
|
404
|
+
),
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Remote templates inherit base-template files that import packages the
|
|
408
|
+
# remote's own pyproject may not declare; re-add the base template's
|
|
409
|
+
# extra_dependencies so the inherited code resolves. Skipped with
|
|
410
|
+
# --skip-deps (reusing a saved config).
|
|
411
|
+
if remote_config and not skip_deps:
|
|
412
|
+
if base_template_name is None:
|
|
413
|
+
raise RuntimeError("remote_config set without a base template")
|
|
414
|
+
base_config = template.load_template_config(
|
|
415
|
+
template.get_template_path(base_template_name)
|
|
416
|
+
)
|
|
417
|
+
base_deps = base_config.get("settings", {}).get("extra_dependencies", [])
|
|
418
|
+
if base_deps:
|
|
419
|
+
template.add_base_template_dependencies(
|
|
420
|
+
project_path,
|
|
421
|
+
base_deps,
|
|
422
|
+
base_template_name,
|
|
423
|
+
auto_approve=auto_approve,
|
|
424
|
+
interactive=interactive,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
except ValueError as e:
|
|
428
|
+
# process_template raises ValueError for input the user can fix.
|
|
429
|
+
raise click.ClickException(str(e)) from e
|
|
430
|
+
|
|
431
|
+
finally:
|
|
432
|
+
if temp_dir_to_clean:
|
|
433
|
+
try:
|
|
434
|
+
shutil.rmtree(temp_dir_to_clean)
|
|
435
|
+
logging.debug("Cleaned up temporary directory: %s", temp_dir_to_clean)
|
|
436
|
+
except OSError as e:
|
|
437
|
+
logging.warning(f"Failed to clean up temporary directory {temp_dir_to_clean}: {e}")
|
|
438
|
+
|
|
439
|
+
# A quiet run's project lives in a temp directory that is deleted moments
|
|
440
|
+
# later, so the next-steps banner would be wrong as well as noisy.
|
|
441
|
+
if quiet:
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
if spec_lines:
|
|
445
|
+
console.print(f"Copied the OpenAPI specs {API_POLICY_FILENAME} references:", style="cyan")
|
|
446
|
+
for line in spec_lines:
|
|
447
|
+
console.print(f" {line}", style="cyan")
|
|
448
|
+
|
|
449
|
+
if params.has_api_policy and params.example_call is None and not in_folder:
|
|
450
|
+
tools_dir = (agent_directory or _rendered_agent_directory(rendered_path)) + "/tools"
|
|
451
|
+
console.print(
|
|
452
|
+
f"Note: {tools_dir}/example_api.py was not generated: the first API in "
|
|
453
|
+
f"{API_POLICY_FILENAME} allows no operation the example could make (lint and "
|
|
454
|
+
"the project's policy test would refuse it). Write your tools with their calls "
|
|
455
|
+
"declared in API_CALLS (see 'Outbound API access' in README.md).",
|
|
456
|
+
style="yellow",
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
_print_next_steps(
|
|
460
|
+
in_folder=in_folder,
|
|
461
|
+
destination_dir=destination_dir,
|
|
462
|
+
project_name=project_name,
|
|
463
|
+
output_dir=output_dir,
|
|
464
|
+
params=params,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# ---------------------------------------------------------------------------
|
|
469
|
+
# Project name and destination
|
|
470
|
+
# ---------------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _resolve_project_name(
|
|
474
|
+
project_name: str,
|
|
475
|
+
*,
|
|
476
|
+
interactive: bool,
|
|
477
|
+
auto_approve: bool,
|
|
478
|
+
output_dir: str | None,
|
|
479
|
+
) -> str:
|
|
480
|
+
"""Resolve, validate, and normalize the project name.
|
|
481
|
+
|
|
482
|
+
Prompts in interactive mode, defaults to "my-agent" under --auto-approve,
|
|
483
|
+
and errors in strict programmatic mode when no name is supplied.
|
|
484
|
+
"""
|
|
485
|
+
if not project_name:
|
|
486
|
+
if interactive:
|
|
487
|
+
project_name = _prompt_for_project_name(output_dir=output_dir)
|
|
488
|
+
elif auto_approve:
|
|
489
|
+
project_name = "my-agent"
|
|
490
|
+
console.print(
|
|
491
|
+
f"Info: Project name not specified. Defaulting to '{project_name}' in auto-approve mode.",
|
|
492
|
+
style="yellow",
|
|
493
|
+
)
|
|
494
|
+
else:
|
|
495
|
+
raise click.UsageError(
|
|
496
|
+
"project-name is a required argument in programmatic mode.\n"
|
|
497
|
+
"You can also use -i / --interactive for interactive mode or --auto-approve / --yes to select defaults."
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
errors = _validate_project_name(project_name)
|
|
501
|
+
if errors:
|
|
502
|
+
raise click.UsageError("\n".join(errors))
|
|
503
|
+
|
|
504
|
+
return normalize_project_name(project_name)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _prompt_for_project_name(output_dir: str | None) -> str:
|
|
508
|
+
"""Interactively prompt for a valid, unique project name."""
|
|
509
|
+
check_dir = (pathlib.Path(output_dir) if output_dir else pathlib.Path.cwd()).resolve()
|
|
510
|
+
while True:
|
|
511
|
+
project_name = Prompt.ask(
|
|
512
|
+
"\n> Enter a name for your project",
|
|
513
|
+
default="my-agent",
|
|
514
|
+
show_default=True,
|
|
515
|
+
)
|
|
516
|
+
errors = _validate_project_name(project_name)
|
|
517
|
+
if errors:
|
|
518
|
+
for error in errors:
|
|
519
|
+
console.print(f"Error: {error}", style="bold red")
|
|
520
|
+
continue
|
|
521
|
+
normalized_name = normalize_project_name(project_name)
|
|
522
|
+
if (check_dir / normalized_name).exists():
|
|
523
|
+
console.print(
|
|
524
|
+
f"Error: Project directory '{check_dir / normalized_name}' already exists. Please choose a different name.",
|
|
525
|
+
style="bold red",
|
|
526
|
+
)
|
|
527
|
+
continue
|
|
528
|
+
return project_name
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _validate_project_name(project_name: str) -> list[str]:
|
|
532
|
+
"""Validate a project name; returns a list of errors (empty when valid)."""
|
|
533
|
+
errors = []
|
|
534
|
+
if len(project_name) > 26:
|
|
535
|
+
errors.append(
|
|
536
|
+
f"Project name '{project_name}' exceeds 26 characters. Please use a shorter name."
|
|
537
|
+
)
|
|
538
|
+
if not re.match(r"^[A-Za-z0-9][A-Za-z0-9_-]*$", project_name):
|
|
539
|
+
errors.append(
|
|
540
|
+
f"Project name '{project_name}' must start with a letter or digit and contain "
|
|
541
|
+
"only letters, digits, hyphens and underscores (it becomes the Helm release name)."
|
|
542
|
+
)
|
|
543
|
+
return errors
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def normalize_project_name(project_name: str) -> str:
|
|
547
|
+
"""Normalize the project name (lowercase, hyphens) for Helm releases and namespaces."""
|
|
548
|
+
needs_normalization = any(char.isupper() for char in project_name) or "_" in project_name
|
|
549
|
+
|
|
550
|
+
if needs_normalization:
|
|
551
|
+
normalized_name = project_name
|
|
552
|
+
console.print(
|
|
553
|
+
"Note: Project names are normalized (lowercase, hyphens only) because they name "
|
|
554
|
+
"the Helm release and the Kubernetes namespaces.",
|
|
555
|
+
style="dim",
|
|
556
|
+
)
|
|
557
|
+
if any(char.isupper() for char in normalized_name):
|
|
558
|
+
normalized_name = normalized_name.lower()
|
|
559
|
+
console.print(
|
|
560
|
+
f"Info: Converting to lowercase for compatibility: '{project_name}' -> '{normalized_name}'",
|
|
561
|
+
style="bold yellow",
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
if "_" in normalized_name:
|
|
565
|
+
name_before_hyphenation = normalized_name
|
|
566
|
+
normalized_name = normalized_name.replace("_", "-")
|
|
567
|
+
console.print(
|
|
568
|
+
f"Info: Replacing underscores with hyphens for compatibility: '{name_before_hyphenation}' -> '{normalized_name}'",
|
|
569
|
+
style="yellow",
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
return normalized_name
|
|
573
|
+
|
|
574
|
+
return project_name
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _prepare_project_path(
|
|
578
|
+
destination_dir: pathlib.Path,
|
|
579
|
+
project_name: str,
|
|
580
|
+
*,
|
|
581
|
+
in_folder: bool,
|
|
582
|
+
auto_approve: bool,
|
|
583
|
+
interactive: bool,
|
|
584
|
+
) -> pathlib.Path:
|
|
585
|
+
"""Resolve the project path and prepare the destination.
|
|
586
|
+
|
|
587
|
+
In-folder mode backs up the existing directory (aborting cleanly if the user
|
|
588
|
+
declines); otherwise verifies the target does not already exist.
|
|
589
|
+
"""
|
|
590
|
+
if in_folder:
|
|
591
|
+
from ..utils.backup import create_project_backup
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
create_project_backup(
|
|
595
|
+
destination_dir,
|
|
596
|
+
console=console,
|
|
597
|
+
auto_approve=auto_approve,
|
|
598
|
+
interactive=interactive,
|
|
599
|
+
)
|
|
600
|
+
except click.Abort:
|
|
601
|
+
console.print("✋ [red]Operation cancelled.[/red]")
|
|
602
|
+
raise
|
|
603
|
+
|
|
604
|
+
console.print()
|
|
605
|
+
return destination_dir
|
|
606
|
+
|
|
607
|
+
project_path = destination_dir / project_name
|
|
608
|
+
if project_path.exists():
|
|
609
|
+
raise click.UsageError(f"Project directory '{project_path}' already exists")
|
|
610
|
+
return project_path
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _preflight() -> None:
|
|
614
|
+
"""Warn about missing local tooling; never blocks scaffolding."""
|
|
615
|
+
if shutil.which("uv") is None:
|
|
616
|
+
console.print(
|
|
617
|
+
"⚠️ uv is not on PATH. Install it (https://docs.astral.sh/uv/) before "
|
|
618
|
+
"`graph-agents-cli install`.",
|
|
619
|
+
style="yellow",
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
# ---------------------------------------------------------------------------
|
|
624
|
+
# Template selection
|
|
625
|
+
# ---------------------------------------------------------------------------
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _resolve_template(
|
|
629
|
+
agent: str | None,
|
|
630
|
+
*,
|
|
631
|
+
deployment_target: str | None,
|
|
632
|
+
interactive: bool,
|
|
633
|
+
auto_approve: bool,
|
|
634
|
+
locked: bool,
|
|
635
|
+
project_name: str,
|
|
636
|
+
base_template: str | None,
|
|
637
|
+
cli_overrides: dict | None,
|
|
638
|
+
) -> tuple[AgentSelection, LoadedTemplateConfig] | None:
|
|
639
|
+
"""Resolve the agent selection and load its (possibly remote) template config."""
|
|
640
|
+
agent_selection = _select_agent(
|
|
641
|
+
agent,
|
|
642
|
+
deployment_target=deployment_target,
|
|
643
|
+
interactive=interactive,
|
|
644
|
+
auto_approve=auto_approve,
|
|
645
|
+
locked=locked,
|
|
646
|
+
project_name=project_name,
|
|
647
|
+
)
|
|
648
|
+
if agent_selection is None:
|
|
649
|
+
return None
|
|
650
|
+
logging.debug("Selected agent: %s", agent_selection.final_agent)
|
|
651
|
+
|
|
652
|
+
loaded_template_config = _load_template_config(
|
|
653
|
+
agent_selection,
|
|
654
|
+
base_template=base_template,
|
|
655
|
+
cli_overrides=cli_overrides,
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
return agent_selection, loaded_template_config
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _select_agent(
|
|
662
|
+
agent: str | None,
|
|
663
|
+
*,
|
|
664
|
+
deployment_target: str | None,
|
|
665
|
+
interactive: bool,
|
|
666
|
+
auto_approve: bool,
|
|
667
|
+
locked: bool,
|
|
668
|
+
project_name: str,
|
|
669
|
+
) -> AgentSelection | None:
|
|
670
|
+
"""Resolve which agent to use and fetch its template if remote/local."""
|
|
671
|
+
if agent:
|
|
672
|
+
return _resolve_specified_agent(agent, locked=locked, project_name=project_name)
|
|
673
|
+
|
|
674
|
+
return _select_agent_interactively(
|
|
675
|
+
deployment_target=deployment_target,
|
|
676
|
+
interactive=interactive,
|
|
677
|
+
auto_approve=auto_approve,
|
|
678
|
+
locked=locked,
|
|
679
|
+
project_name=project_name,
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _resolve_specified_agent(
|
|
684
|
+
agent: str,
|
|
685
|
+
*,
|
|
686
|
+
locked: bool,
|
|
687
|
+
project_name: str,
|
|
688
|
+
) -> AgentSelection | None:
|
|
689
|
+
"""Resolve an explicit ``--agent`` value: local@ path, remote spec, or bundled name/number."""
|
|
690
|
+
if agent.startswith("local@"):
|
|
691
|
+
return _resolve_local_spec(agent, locked=locked, project_name=project_name)
|
|
692
|
+
|
|
693
|
+
remote = _resolve_remote_spec(agent, locked=locked, project_name=project_name)
|
|
694
|
+
if remote:
|
|
695
|
+
return remote
|
|
696
|
+
|
|
697
|
+
agents = template.get_available_agents(include_hidden=True)
|
|
698
|
+
if any(p["name"] == agent for p in agents.values()):
|
|
699
|
+
selected_agent = agent
|
|
700
|
+
else:
|
|
701
|
+
try:
|
|
702
|
+
agent_num = int(agent)
|
|
703
|
+
if agent_num in agents:
|
|
704
|
+
selected_agent = agents[agent_num]["name"]
|
|
705
|
+
else:
|
|
706
|
+
raise ValueError(f"Invalid agent number: {agent_num}")
|
|
707
|
+
except ValueError as err:
|
|
708
|
+
available = ", ".join(a["name"] for a in agents.values())
|
|
709
|
+
raise click.UsageError(
|
|
710
|
+
f"Invalid agent name or number: {agent}. Available: {available}"
|
|
711
|
+
) from err
|
|
712
|
+
|
|
713
|
+
return AgentSelection(
|
|
714
|
+
agent=agent,
|
|
715
|
+
final_agent=selected_agent,
|
|
716
|
+
template_source_path=None,
|
|
717
|
+
temp_dir_to_clean=None,
|
|
718
|
+
remote_spec=None,
|
|
719
|
+
recorded_spec=None,
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _resolve_local_spec(
|
|
724
|
+
agent: str,
|
|
725
|
+
*,
|
|
726
|
+
locked: bool,
|
|
727
|
+
project_name: str,
|
|
728
|
+
) -> AgentSelection | None:
|
|
729
|
+
"""Resolve a ``local@<path>`` spec: copy the template to a temp dir."""
|
|
730
|
+
path_str = agent.split("@", 1)[1]
|
|
731
|
+
local_path = pathlib.Path(path_str).resolve()
|
|
732
|
+
if not local_path.is_dir():
|
|
733
|
+
raise click.ClickException(f"Local path not found or not a directory: {local_path}")
|
|
734
|
+
|
|
735
|
+
# Record the absolute path: a relative local path would resolve against
|
|
736
|
+
# wherever enhance / upgrade are later run from.
|
|
737
|
+
recorded_spec = f"local@{local_path}"
|
|
738
|
+
|
|
739
|
+
temp_dir = tempfile.mkdtemp(prefix="gacli_local_template_")
|
|
740
|
+
template_source_path = pathlib.Path(temp_dir) / local_path.name
|
|
741
|
+
shutil.copytree(local_path, template_source_path, ignore=standard_ignore_patterns)
|
|
742
|
+
|
|
743
|
+
if remote_template.check_and_execute_with_version_lock(
|
|
744
|
+
template_source_path, agent, locked, project_name
|
|
745
|
+
):
|
|
746
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
747
|
+
return None
|
|
748
|
+
|
|
749
|
+
if locked:
|
|
750
|
+
console.print("✅ Using version-locked template", style="green")
|
|
751
|
+
else:
|
|
752
|
+
console.print(f"Using local template: {local_path}")
|
|
753
|
+
logging.debug("Copied local template to temporary dir: %s", template_source_path)
|
|
754
|
+
return AgentSelection(
|
|
755
|
+
agent=agent,
|
|
756
|
+
final_agent=f"local_{template_source_path.name}",
|
|
757
|
+
template_source_path=template_source_path,
|
|
758
|
+
temp_dir_to_clean=temp_dir,
|
|
759
|
+
remote_spec=None,
|
|
760
|
+
recorded_spec=recorded_spec,
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _resolve_remote_spec(
|
|
765
|
+
agent: str,
|
|
766
|
+
*,
|
|
767
|
+
locked: bool,
|
|
768
|
+
project_name: str,
|
|
769
|
+
) -> AgentSelection | None:
|
|
770
|
+
"""Fetch a remote-template spec when ``agent`` names one; None otherwise."""
|
|
771
|
+
remote_spec = remote_template.parse_agent_spec(agent)
|
|
772
|
+
if not remote_spec:
|
|
773
|
+
return None
|
|
774
|
+
|
|
775
|
+
console.print(f"Fetching remote template: {agent}")
|
|
776
|
+
template_source_path, temp_dir_path = remote_template.fetch_remote_template(
|
|
777
|
+
remote_spec, agent, locked, project_name
|
|
778
|
+
)
|
|
779
|
+
return AgentSelection(
|
|
780
|
+
agent=agent,
|
|
781
|
+
final_agent=f"remote_{hash(agent)}",
|
|
782
|
+
template_source_path=template_source_path,
|
|
783
|
+
temp_dir_to_clean=str(temp_dir_path),
|
|
784
|
+
remote_spec=remote_spec,
|
|
785
|
+
recorded_spec=agent,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _select_agent_interactively(
|
|
790
|
+
*,
|
|
791
|
+
deployment_target: str | None,
|
|
792
|
+
interactive: bool,
|
|
793
|
+
auto_approve: bool,
|
|
794
|
+
locked: bool,
|
|
795
|
+
project_name: str,
|
|
796
|
+
) -> AgentSelection:
|
|
797
|
+
"""Select an agent when no explicit ``--agent`` was given.
|
|
798
|
+
|
|
799
|
+
Prompts interactively; otherwise defaults to ``langgraph`` (or the first
|
|
800
|
+
available agent) and says so.
|
|
801
|
+
"""
|
|
802
|
+
agents = template.get_available_agents(deployment_target=deployment_target)
|
|
803
|
+
|
|
804
|
+
if interactive:
|
|
805
|
+
final_agent = display_agent_selection(deployment_target)
|
|
806
|
+
else:
|
|
807
|
+
if not agents:
|
|
808
|
+
raise click.ClickException(
|
|
809
|
+
"Error: No agents available for the selected deployment target."
|
|
810
|
+
)
|
|
811
|
+
names = [a["name"] for a in agents.values()]
|
|
812
|
+
final_agent = DEFAULT_AGENT if DEFAULT_AGENT in names else names[0]
|
|
813
|
+
console.print(
|
|
814
|
+
f"Info: --agent not specified. Defaulting to '{final_agent}'.",
|
|
815
|
+
style="yellow",
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
# A browse result may itself be a spec: process it like CLI input.
|
|
819
|
+
if final_agent.startswith("local@"):
|
|
820
|
+
local = _resolve_local_spec(final_agent, locked=locked, project_name=project_name)
|
|
821
|
+
if local is None:
|
|
822
|
+
raise click.ClickException("Version-locked template executed a nested command.")
|
|
823
|
+
return local
|
|
824
|
+
remote = _resolve_remote_spec(final_agent, locked=locked, project_name=project_name)
|
|
825
|
+
if remote:
|
|
826
|
+
return remote
|
|
827
|
+
|
|
828
|
+
return AgentSelection(
|
|
829
|
+
agent=final_agent,
|
|
830
|
+
final_agent=final_agent,
|
|
831
|
+
template_source_path=None,
|
|
832
|
+
temp_dir_to_clean=None,
|
|
833
|
+
remote_spec=None,
|
|
834
|
+
recorded_spec=None,
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def display_agent_selection(deployment_target: str | None = None) -> str:
|
|
839
|
+
"""Display the bundled agents and prompt for a selection (or a custom spec)."""
|
|
840
|
+
agents = template.get_available_agents(deployment_target=deployment_target)
|
|
841
|
+
if not agents:
|
|
842
|
+
if deployment_target:
|
|
843
|
+
raise click.ClickException(
|
|
844
|
+
f"No agents available for deployment target '{deployment_target}'"
|
|
845
|
+
)
|
|
846
|
+
raise click.ClickException("No valid agents found")
|
|
847
|
+
|
|
848
|
+
console.print("\n> Please select an agent to get started:")
|
|
849
|
+
console.print("\n [bold cyan]🐍 Python[/]")
|
|
850
|
+
for num, agent in agents.items():
|
|
851
|
+
display_name = agent.get("display_name", agent["name"])
|
|
852
|
+
console.print(
|
|
853
|
+
f" {num}. [bold]{display_name.ljust(14)}[/] [dim]{agent['description']}[/]"
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
custom_num = len(agents) + 1
|
|
857
|
+
console.print("\n [bold cyan]🔧 More Options[/]")
|
|
858
|
+
console.print(
|
|
859
|
+
f" {custom_num}. [bold]{'Custom'.ljust(14)}[/] "
|
|
860
|
+
"[dim]local@/path/to/template or org/repo/path@ref[/]"
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
while True:
|
|
864
|
+
choice = IntPrompt.ask(
|
|
865
|
+
"\nEnter the number of your template choice", default=1, show_default=True
|
|
866
|
+
)
|
|
867
|
+
if choice in agents:
|
|
868
|
+
return agents[choice]["name"]
|
|
869
|
+
if choice == custom_num:
|
|
870
|
+
spec = Prompt.ask("\nEnter the template spec (local@<path> or a remote spec)").strip()
|
|
871
|
+
if spec.startswith("local@") or remote_template.parse_agent_spec(spec):
|
|
872
|
+
return spec
|
|
873
|
+
console.print(f"Invalid template spec: {spec}", style="bold red")
|
|
874
|
+
continue
|
|
875
|
+
console.print(f"Invalid agent selection: {choice}", style="bold red")
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def _load_template_config(
|
|
879
|
+
selection: AgentSelection,
|
|
880
|
+
*,
|
|
881
|
+
base_template: str | None,
|
|
882
|
+
cli_overrides: dict | None,
|
|
883
|
+
) -> LoadedTemplateConfig:
|
|
884
|
+
"""Load and merge the selected template's config.
|
|
885
|
+
|
|
886
|
+
For remote / local-path templates, loads the remote config, merges it over
|
|
887
|
+
the inherited base-template config, and derives the deployment agent name
|
|
888
|
+
from the base template. For bundled agents, loads the local config and
|
|
889
|
+
applies any CLI overrides.
|
|
890
|
+
"""
|
|
891
|
+
final_agent = selection.final_agent
|
|
892
|
+
template_source_path = selection.template_source_path
|
|
893
|
+
remote_spec = selection.remote_spec
|
|
894
|
+
|
|
895
|
+
base_template_name = None
|
|
896
|
+
if template_source_path:
|
|
897
|
+
if cli_overrides is None:
|
|
898
|
+
cli_overrides = {}
|
|
899
|
+
|
|
900
|
+
if base_template:
|
|
901
|
+
if not template.validate_base_template(base_template):
|
|
902
|
+
available_templates = template.get_available_base_templates()
|
|
903
|
+
if selection.temp_dir_to_clean:
|
|
904
|
+
shutil.rmtree(selection.temp_dir_to_clean, ignore_errors=True)
|
|
905
|
+
raise click.UsageError(
|
|
906
|
+
f"Base template '{base_template}' not found.\n"
|
|
907
|
+
f"Available base templates: {', '.join(available_templates)}"
|
|
908
|
+
)
|
|
909
|
+
cli_overrides["base_template"] = base_template
|
|
910
|
+
|
|
911
|
+
source_config = remote_template.load_remote_template_config(
|
|
912
|
+
template_source_path, cli_overrides
|
|
913
|
+
)
|
|
914
|
+
if source_config:
|
|
915
|
+
logging.debug("Final remote template config: %s", source_config)
|
|
916
|
+
|
|
917
|
+
base_template_name = remote_template.get_base_template_name(source_config)
|
|
918
|
+
logging.debug("Using base template: %s", base_template_name)
|
|
919
|
+
|
|
920
|
+
base_config = template.load_template_config(
|
|
921
|
+
template.agents_dir() / base_template_name / ".template"
|
|
922
|
+
)
|
|
923
|
+
config = remote_template.merge_template_configs(base_config, source_config)
|
|
924
|
+
template_path = template_source_path / ".template"
|
|
925
|
+
else:
|
|
926
|
+
template_path = template.agents_dir() / final_agent / ".template"
|
|
927
|
+
config = template.load_template_config(template_path)
|
|
928
|
+
if cli_overrides:
|
|
929
|
+
config = remote_template.merge_template_configs(config, cli_overrides)
|
|
930
|
+
logging.debug("Applied CLI overrides to local template config: %s", cli_overrides)
|
|
931
|
+
|
|
932
|
+
if final_agent and not remote_spec and config.get("hidden", False):
|
|
933
|
+
console.print(
|
|
934
|
+
f"Warning: '{final_agent}' is an experimental template and is not fully supported.",
|
|
935
|
+
style="yellow",
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
deployment_agent_name = final_agent
|
|
939
|
+
remote_config = None
|
|
940
|
+
if template_source_path:
|
|
941
|
+
deployment_agent_name = remote_template.get_base_template_name(config)
|
|
942
|
+
remote_config = config
|
|
943
|
+
|
|
944
|
+
return LoadedTemplateConfig(
|
|
945
|
+
config=config,
|
|
946
|
+
template_path=template_path,
|
|
947
|
+
base_template_name=base_template_name,
|
|
948
|
+
deployment_agent_name=deployment_agent_name,
|
|
949
|
+
remote_config=remote_config,
|
|
950
|
+
cli_overrides=cli_overrides,
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
# ---------------------------------------------------------------------------
|
|
955
|
+
# Create parameters (runtime, provider, model, checkpointer, target, cd, ...)
|
|
956
|
+
# ---------------------------------------------------------------------------
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def _resolve_create_params(
|
|
960
|
+
*,
|
|
961
|
+
deployment_target: str | None,
|
|
962
|
+
runtime: str | None,
|
|
963
|
+
model_provider: str | None,
|
|
964
|
+
model: str | None,
|
|
965
|
+
checkpointer: str | None,
|
|
966
|
+
registry: str | None,
|
|
967
|
+
cd: str | None,
|
|
968
|
+
auth_policy: str | None,
|
|
969
|
+
api_document: dict | None,
|
|
970
|
+
api_policy_dir: pathlib.Path | None,
|
|
971
|
+
process: str | None,
|
|
972
|
+
prototype: bool,
|
|
973
|
+
interactive: bool,
|
|
974
|
+
auto_approve: bool,
|
|
975
|
+
quiet: bool,
|
|
976
|
+
skip_checks: bool,
|
|
977
|
+
deployment_agent_name: str,
|
|
978
|
+
remote_config: dict | None,
|
|
979
|
+
git_dir: pathlib.Path,
|
|
980
|
+
) -> CreateParams:
|
|
981
|
+
"""Resolve every create parameter (flag > prompt > default) and validate the combination."""
|
|
982
|
+
final_deployment = _resolve_deployment_target(
|
|
983
|
+
deployment_target=deployment_target,
|
|
984
|
+
prototype=prototype,
|
|
985
|
+
deployment_agent_name=deployment_agent_name,
|
|
986
|
+
remote_config=remote_config,
|
|
987
|
+
interactive=interactive,
|
|
988
|
+
auto_approve=auto_approve,
|
|
989
|
+
quiet=quiet,
|
|
990
|
+
)
|
|
991
|
+
logging.debug("Selected deployment target: %s", final_deployment)
|
|
992
|
+
|
|
993
|
+
final_runtime = runtime or (
|
|
994
|
+
template.prompt_runtime(DEFAULT_RUNTIME) if interactive else DEFAULT_RUNTIME
|
|
995
|
+
)
|
|
996
|
+
final_provider = model_provider or (
|
|
997
|
+
template.prompt_model_provider(DEFAULT_MODEL_PROVIDER)
|
|
998
|
+
if interactive
|
|
999
|
+
else DEFAULT_MODEL_PROVIDER
|
|
1000
|
+
)
|
|
1001
|
+
default_model = DEFAULT_MODELS.get(final_provider, "")
|
|
1002
|
+
if model:
|
|
1003
|
+
final_model = model
|
|
1004
|
+
elif interactive:
|
|
1005
|
+
final_model = (
|
|
1006
|
+
Prompt.ask("\n> Model name", default=default_model, show_default=True).strip()
|
|
1007
|
+
or default_model
|
|
1008
|
+
)
|
|
1009
|
+
else:
|
|
1010
|
+
final_model = default_model
|
|
1011
|
+
|
|
1012
|
+
default_checkpointer = template.default_checkpointer(final_deployment)
|
|
1013
|
+
final_checkpointer = checkpointer or (
|
|
1014
|
+
template.prompt_checkpointer(default_checkpointer) if interactive else default_checkpointer
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
final_cd = _resolve_cd(
|
|
1018
|
+
cd=cd,
|
|
1019
|
+
prototype=prototype,
|
|
1020
|
+
final_deployment=final_deployment,
|
|
1021
|
+
interactive=interactive,
|
|
1022
|
+
auto_approve=auto_approve,
|
|
1023
|
+
quiet=quiet,
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
try:
|
|
1027
|
+
template.validate_combination(final_runtime, final_checkpointer, final_deployment, final_cd)
|
|
1028
|
+
except ValueError as e:
|
|
1029
|
+
raise click.UsageError(str(e)) from e
|
|
1030
|
+
|
|
1031
|
+
final_registry = _resolve_registry(
|
|
1032
|
+
registry=registry,
|
|
1033
|
+
final_deployment=final_deployment,
|
|
1034
|
+
interactive=interactive,
|
|
1035
|
+
quiet=quiet,
|
|
1036
|
+
git_dir=git_dir,
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
final_auth_policy = auth_policy or (
|
|
1040
|
+
template.prompt_auth_policy(DEFAULT_AUTH_POLICY) if interactive else DEFAULT_AUTH_POLICY
|
|
1041
|
+
)
|
|
1042
|
+
if final_auth_policy == "custom" and not quiet:
|
|
1043
|
+
console.print(
|
|
1044
|
+
"Info: custom ships as a fail-closed stub: implement CustomPolicy in "
|
|
1045
|
+
"app/policies/custom.py and set auth_policy_implemented: true before "
|
|
1046
|
+
"`deploy --env staging|prod`.",
|
|
1047
|
+
style="cyan",
|
|
1048
|
+
)
|
|
1049
|
+
elif final_auth_policy == "jwt" and not quiet:
|
|
1050
|
+
console.print(
|
|
1051
|
+
"Info: jwt verifies OIDC bearer tokens: set AUTH_JWT_JWKS_URL (or "
|
|
1052
|
+
"AUTH_JWT_PUBLIC_KEY), AUTH_JWT_ISSUER and AUTH_JWT_AUDIENCE (see .env.example). "
|
|
1053
|
+
"For local runs, `graph-agents-cli auth dev-token --sub <user>` sets a dev key and "
|
|
1054
|
+
"prints a token for GRAPH_AGENTS_CLI_API_KEY.",
|
|
1055
|
+
style="cyan",
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
apis = _api_policy.summarize(api_document) if api_document else ()
|
|
1059
|
+
problem = _api_policy.forward_runtime_problem(apis, final_runtime)
|
|
1060
|
+
if problem:
|
|
1061
|
+
raise click.UsageError(problem)
|
|
1062
|
+
|
|
1063
|
+
return CreateParams(
|
|
1064
|
+
deployment_target=final_deployment,
|
|
1065
|
+
runtime=final_runtime,
|
|
1066
|
+
model_provider=final_provider,
|
|
1067
|
+
model=final_model,
|
|
1068
|
+
checkpointer=final_checkpointer,
|
|
1069
|
+
registry=final_registry,
|
|
1070
|
+
cd=final_cd,
|
|
1071
|
+
auth_policy=final_auth_policy,
|
|
1072
|
+
has_api_policy=api_document is not None,
|
|
1073
|
+
process=(process or "").strip() or None,
|
|
1074
|
+
# Every bearer API's token variable joins secrets.keys.
|
|
1075
|
+
apis=apis,
|
|
1076
|
+
# A relative openapi: path is read next to the seed policy.
|
|
1077
|
+
example_call=(
|
|
1078
|
+
policy_check.example_call(api_document, base_dir=api_policy_dir)
|
|
1079
|
+
if api_document
|
|
1080
|
+
else None
|
|
1081
|
+
),
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def _read_project_policy(
|
|
1086
|
+
path: pathlib.Path,
|
|
1087
|
+
) -> tuple[tuple[_api_policy.ApiSummary, ...], _api_policy.ExampleCall | None]:
|
|
1088
|
+
"""The APIs and the example call of a project's own policy (empty when unreadable).
|
|
1089
|
+
|
|
1090
|
+
A relative ``openapi:`` path is resolved against the project root, as lint does.
|
|
1091
|
+
"""
|
|
1092
|
+
document = _api_policy.read_policy_document(path)
|
|
1093
|
+
if document is None:
|
|
1094
|
+
return (), None
|
|
1095
|
+
return _api_policy.summarize(document), policy_check.example_call(
|
|
1096
|
+
document, base_dir=path.parent
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
def _rendered_agent_directory(project_dir: pathlib.Path) -> str:
|
|
1101
|
+
"""The agent directory the rendered manifest records (``app`` when unreadable)."""
|
|
1102
|
+
try:
|
|
1103
|
+
return read_project_config(str(project_dir)).agent_directory or "app"
|
|
1104
|
+
except click.ClickException:
|
|
1105
|
+
return "app"
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
# ``carried`` value of a project that had no manifest before an in-folder render.
|
|
1109
|
+
_NO_MANIFEST = object()
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def _recorded_build_block(project_dir: pathlib.Path) -> object:
|
|
1113
|
+
"""The ``cli_build`` value an existing manifest holds (``_NO_MANIFEST`` without one)."""
|
|
1114
|
+
if not (project_dir / MANIFEST_FILENAME).is_file():
|
|
1115
|
+
return _NO_MANIFEST
|
|
1116
|
+
return build_record.read_manifest_data(project_dir).get(build_record.MANIFEST_KEY)
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
def _record_build(
|
|
1120
|
+
rendered_path: pathlib.Path, *, carried: object, render_is_snapshot: bool
|
|
1121
|
+
) -> None:
|
|
1122
|
+
"""Write ``cli_build``: the running build, or the one an in-folder render must keep.
|
|
1123
|
+
|
|
1124
|
+
An in-folder re-render of a project keeps the project's files, so they are
|
|
1125
|
+
still those of the build its manifest records: that record (or its absence)
|
|
1126
|
+
is carried over. A new project, or a folder adopted without a manifest,
|
|
1127
|
+
records the running build, with the digest of this render when it is the
|
|
1128
|
+
snapshot `scaffold upgrade` compares against.
|
|
1129
|
+
"""
|
|
1130
|
+
if carried is not _NO_MANIFEST:
|
|
1131
|
+
try:
|
|
1132
|
+
record = build_record.parse_build_record(carried)
|
|
1133
|
+
except build_record.MalformedRecordError as e:
|
|
1134
|
+
logging.warning("Leaving %s as it is: %s", build_record.MANIFEST_KEY, e)
|
|
1135
|
+
return
|
|
1136
|
+
build_record.write_build_record(rendered_path, record)
|
|
1137
|
+
return
|
|
1138
|
+
digest = build_record.template_digest(rendered_path) if render_is_snapshot else None
|
|
1139
|
+
build_record.write_build_record(
|
|
1140
|
+
rendered_path, build_record.BuildRecord.of(build_record.running_build(), digest)
|
|
1141
|
+
)
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
def _carry_recorded_state(project_dir: pathlib.Path, params: CreateParams) -> CreateParams:
|
|
1145
|
+
"""Keep what the existing manifest records where the re-render must not reset it.
|
|
1146
|
+
|
|
1147
|
+
``auth_policy_implemented`` is the developer's flag (flipped once the stub
|
|
1148
|
+
is replaced): it is kept when the policy is unchanged and re-derived only
|
|
1149
|
+
when ``auth_policy`` switches. The APIs of an existing ``api-policy.yaml``
|
|
1150
|
+
are carried so their bearer token variables stay in ``secrets.keys``.
|
|
1151
|
+
"""
|
|
1152
|
+
if not (project_dir / MANIFEST_FILENAME).is_file():
|
|
1153
|
+
return params
|
|
1154
|
+
try:
|
|
1155
|
+
existing = read_project_config(str(project_dir))
|
|
1156
|
+
except click.ClickException as e:
|
|
1157
|
+
logging.warning("Could not read the existing manifest: %s", e)
|
|
1158
|
+
return params
|
|
1159
|
+
updates: dict[str, object] = {}
|
|
1160
|
+
if existing.auth_policy == params.auth_policy:
|
|
1161
|
+
updates["auth_policy_implemented"] = existing.auth_policy_implemented
|
|
1162
|
+
if not params.apis and existing.api_policy_file:
|
|
1163
|
+
apis, example = _read_project_policy(project_dir / existing.api_policy_file)
|
|
1164
|
+
if apis:
|
|
1165
|
+
updates["apis"] = apis
|
|
1166
|
+
updates["example_call"] = example
|
|
1167
|
+
return dataclasses.replace(params, **updates) if updates else params
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
def _resolve_deployment_target(
|
|
1171
|
+
*,
|
|
1172
|
+
deployment_target: str | None,
|
|
1173
|
+
prototype: bool,
|
|
1174
|
+
deployment_agent_name: str,
|
|
1175
|
+
remote_config: dict | None,
|
|
1176
|
+
interactive: bool,
|
|
1177
|
+
auto_approve: bool,
|
|
1178
|
+
quiet: bool,
|
|
1179
|
+
) -> str:
|
|
1180
|
+
"""Resolve the deployment target.
|
|
1181
|
+
|
|
1182
|
+
Honors an explicit --deployment-target, defaults to 'none' in prototype
|
|
1183
|
+
mode, auto-selects when only one target exists, prompts interactively, and
|
|
1184
|
+
otherwise defaults to kubernetes.
|
|
1185
|
+
"""
|
|
1186
|
+
if deployment_target:
|
|
1187
|
+
return deployment_target
|
|
1188
|
+
|
|
1189
|
+
if prototype:
|
|
1190
|
+
if not quiet:
|
|
1191
|
+
console.print("Info: Prototype mode: using deployment_target='none'.", style="yellow")
|
|
1192
|
+
return "none"
|
|
1193
|
+
|
|
1194
|
+
available_targets = template.get_deployment_targets(
|
|
1195
|
+
deployment_agent_name, remote_config=remote_config
|
|
1196
|
+
)
|
|
1197
|
+
if not available_targets:
|
|
1198
|
+
raise click.ClickException(
|
|
1199
|
+
f"Error: No deployment targets available for agent '{deployment_agent_name}'."
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
if len(available_targets) == 1:
|
|
1203
|
+
if not quiet:
|
|
1204
|
+
console.print(
|
|
1205
|
+
f"Info: Using '{available_targets[0]}' (only available deployment target for this agent).",
|
|
1206
|
+
style="yellow",
|
|
1207
|
+
)
|
|
1208
|
+
return available_targets[0]
|
|
1209
|
+
if interactive:
|
|
1210
|
+
return template.prompt_deployment_target(
|
|
1211
|
+
deployment_agent_name,
|
|
1212
|
+
remote_config=remote_config,
|
|
1213
|
+
default_value=DEFAULT_DEPLOYMENT_TARGET,
|
|
1214
|
+
)
|
|
1215
|
+
chosen = (
|
|
1216
|
+
DEFAULT_DEPLOYMENT_TARGET
|
|
1217
|
+
if DEFAULT_DEPLOYMENT_TARGET in available_targets
|
|
1218
|
+
else available_targets[0]
|
|
1219
|
+
)
|
|
1220
|
+
if not quiet:
|
|
1221
|
+
console.print(
|
|
1222
|
+
f"Info: --deployment-target not specified. Defaulting to '{chosen}'.",
|
|
1223
|
+
style="yellow",
|
|
1224
|
+
)
|
|
1225
|
+
return chosen
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def _resolve_cd(
|
|
1229
|
+
*,
|
|
1230
|
+
cd: str | None,
|
|
1231
|
+
prototype: bool,
|
|
1232
|
+
final_deployment: str,
|
|
1233
|
+
interactive: bool,
|
|
1234
|
+
auto_approve: bool,
|
|
1235
|
+
quiet: bool,
|
|
1236
|
+
) -> str:
|
|
1237
|
+
"""Resolve the CD mode.
|
|
1238
|
+
|
|
1239
|
+
--prototype forces 'skip'; a 'none' target only allows 'skip' (an explicit
|
|
1240
|
+
other value is a usage error); otherwise the flag, a prompt, or 'skip'.
|
|
1241
|
+
"""
|
|
1242
|
+
if prototype:
|
|
1243
|
+
if cd and cd != "skip" and not quiet:
|
|
1244
|
+
console.print(
|
|
1245
|
+
f"Info: --cd '{cd}' ignored due to --prototype (CD forced to 'skip').",
|
|
1246
|
+
style="yellow",
|
|
1247
|
+
)
|
|
1248
|
+
logging.debug("Prototype mode: setting cd to 'skip'")
|
|
1249
|
+
return "skip"
|
|
1250
|
+
if final_deployment != "kubernetes":
|
|
1251
|
+
if cd and cd != "skip":
|
|
1252
|
+
raise click.UsageError(
|
|
1253
|
+
f"--cd {cd} requires --deployment-target kubernetes "
|
|
1254
|
+
f"(got deployment_target={final_deployment})."
|
|
1255
|
+
)
|
|
1256
|
+
return "skip"
|
|
1257
|
+
if cd:
|
|
1258
|
+
return cd
|
|
1259
|
+
if interactive:
|
|
1260
|
+
return template.prompt_cd(DEFAULT_CD)
|
|
1261
|
+
if auto_approve and not quiet:
|
|
1262
|
+
console.print(
|
|
1263
|
+
"Info: --cd not specified. Defaulting to 'skip' (deploy from a workstation).",
|
|
1264
|
+
style="yellow",
|
|
1265
|
+
)
|
|
1266
|
+
return DEFAULT_CD
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
def _git_origin_owner(directory: pathlib.Path) -> str | None:
|
|
1270
|
+
"""The owner of the git ``origin`` remote of ``directory``, lowercased, or None."""
|
|
1271
|
+
try:
|
|
1272
|
+
from graph_agents_cli._runner import run_resolved
|
|
1273
|
+
|
|
1274
|
+
result = run_resolved(
|
|
1275
|
+
["git", "remote", "get-url", "origin"],
|
|
1276
|
+
cwd=str(directory),
|
|
1277
|
+
capture_output=True,
|
|
1278
|
+
text=True,
|
|
1279
|
+
check=False,
|
|
1280
|
+
)
|
|
1281
|
+
except Exception as e:
|
|
1282
|
+
logging.debug("git remote lookup failed: %s", e)
|
|
1283
|
+
return None
|
|
1284
|
+
if result.returncode != 0 or not (result.stdout or "").strip():
|
|
1285
|
+
return None
|
|
1286
|
+
return parse_git_remote_owner(result.stdout.strip())
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
def parse_git_remote_owner(url: str) -> str | None:
|
|
1290
|
+
"""Owner segment of a git remote URL (https, ssh:// or scp-like), lowercased."""
|
|
1291
|
+
url = url.strip()
|
|
1292
|
+
if url.endswith(".git"):
|
|
1293
|
+
url = url[: -len(".git")]
|
|
1294
|
+
path = None
|
|
1295
|
+
match = re.match(r"^[A-Za-z][A-Za-z0-9+.-]*://[^/]+/(.+)$", url)
|
|
1296
|
+
if match:
|
|
1297
|
+
path = match.group(1)
|
|
1298
|
+
else:
|
|
1299
|
+
match = re.match(r"^[^@/]+@[^:]+:(.+)$", url)
|
|
1300
|
+
if match:
|
|
1301
|
+
path = match.group(1)
|
|
1302
|
+
if path is None:
|
|
1303
|
+
return None
|
|
1304
|
+
segments = [s for s in path.split("/") if s]
|
|
1305
|
+
if len(segments) < 2:
|
|
1306
|
+
return None
|
|
1307
|
+
owner = segments[0].lower()
|
|
1308
|
+
return owner or None
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
def _resolve_registry(
|
|
1312
|
+
*,
|
|
1313
|
+
registry: str | None,
|
|
1314
|
+
final_deployment: str,
|
|
1315
|
+
interactive: bool,
|
|
1316
|
+
quiet: bool,
|
|
1317
|
+
git_dir: pathlib.Path,
|
|
1318
|
+
) -> str:
|
|
1319
|
+
"""Resolve the container registry (empty when the target is none: nothing is pushed)."""
|
|
1320
|
+
if final_deployment != "kubernetes":
|
|
1321
|
+
if registry and not quiet:
|
|
1322
|
+
console.print(
|
|
1323
|
+
f"Info: --registry '{registry}' ignored for deployment_target='{final_deployment}'.",
|
|
1324
|
+
style="yellow",
|
|
1325
|
+
)
|
|
1326
|
+
return ""
|
|
1327
|
+
if registry:
|
|
1328
|
+
return registry.strip().rstrip("/")
|
|
1329
|
+
|
|
1330
|
+
owner = _git_origin_owner(git_dir)
|
|
1331
|
+
detected = f"{DEFAULT_REGISTRY_HOST}/{owner}" if owner else None
|
|
1332
|
+
|
|
1333
|
+
if interactive:
|
|
1334
|
+
value = Prompt.ask(
|
|
1335
|
+
"\n> Container registry (<host>/<org>)",
|
|
1336
|
+
default=detected or DEFAULT_REGISTRY_PLACEHOLDER,
|
|
1337
|
+
show_default=True,
|
|
1338
|
+
).strip()
|
|
1339
|
+
return (value or detected or DEFAULT_REGISTRY_PLACEHOLDER).rstrip("/")
|
|
1340
|
+
|
|
1341
|
+
if detected:
|
|
1342
|
+
if not quiet:
|
|
1343
|
+
console.print(
|
|
1344
|
+
f"Info: --registry not specified. Using '{detected}' from the git origin remote.",
|
|
1345
|
+
style="yellow",
|
|
1346
|
+
)
|
|
1347
|
+
return detected
|
|
1348
|
+
|
|
1349
|
+
if not quiet:
|
|
1350
|
+
console.print(
|
|
1351
|
+
f"⚠️ --registry not specified and no git origin remote found; using the "
|
|
1352
|
+
f"placeholder '{DEFAULT_REGISTRY_PLACEHOLDER}', which `build` and `deploy` refuse. "
|
|
1353
|
+
f"Re-run with --registry <host>/<org>, or later run `{REGISTRY_FIX_COMMAND}` in "
|
|
1354
|
+
f"the project ({REGISTRY_FIX_EFFECT}).",
|
|
1355
|
+
style="yellow",
|
|
1356
|
+
)
|
|
1357
|
+
return DEFAULT_REGISTRY_PLACEHOLDER
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
# ---------------------------------------------------------------------------
|
|
1361
|
+
# Next steps
|
|
1362
|
+
# ---------------------------------------------------------------------------
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
def _print_next_steps(
|
|
1366
|
+
*,
|
|
1367
|
+
in_folder: bool,
|
|
1368
|
+
destination_dir: pathlib.Path,
|
|
1369
|
+
project_name: str,
|
|
1370
|
+
output_dir: str | None,
|
|
1371
|
+
params: CreateParams,
|
|
1372
|
+
) -> None:
|
|
1373
|
+
"""Print the post-creation success banner and next-step hints."""
|
|
1374
|
+
if not in_folder:
|
|
1375
|
+
cd_path = (destination_dir / project_name) if output_dir else project_name
|
|
1376
|
+
else:
|
|
1377
|
+
cd_path = "."
|
|
1378
|
+
|
|
1379
|
+
console.print("\n[bold green]✅ Success![/] Your agent project is ready.\n")
|
|
1380
|
+
|
|
1381
|
+
console.print("[bold cyan]📖 Documentation[/]")
|
|
1382
|
+
console.print(f" README: [cyan]cat {cd_path}/README.md[/]")
|
|
1383
|
+
|
|
1384
|
+
if params.deployment_target == "none":
|
|
1385
|
+
console.print(
|
|
1386
|
+
"\n[bold cyan]💡 Tip[/]\n"
|
|
1387
|
+
" Add a deployment target later with: "
|
|
1388
|
+
"[cyan]graph-agents-cli scaffold enhance --deployment-target kubernetes[/]"
|
|
1389
|
+
)
|
|
1390
|
+
elif params.cd == "skip":
|
|
1391
|
+
console.print(
|
|
1392
|
+
"\n[bold cyan]💡 Tip[/]\n"
|
|
1393
|
+
" Add continuous delivery later with: "
|
|
1394
|
+
"[cyan]graph-agents-cli scaffold enhance --cd argocd|helm-push[/]"
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1397
|
+
console.print("\n[bold cyan]🚀 Get Started[/]")
|
|
1398
|
+
console.print(f" [bold bright_green]cd {cd_path}[/]")
|
|
1399
|
+
# The local server refuses every request until .env has the provider key
|
|
1400
|
+
# and (shared-bearer) an API_KEY; `login --write-env` fills both in.
|
|
1401
|
+
console.print(" [bold bright_green]cp .env.example .env[/]")
|
|
1402
|
+
console.print(" [bold bright_green]graph-agents-cli login --write-env[/]")
|
|
1403
|
+
console.print(" [bold bright_green]graph-agents-cli install[/]")
|
|
1404
|
+
if params.auth_policy == "jwt":
|
|
1405
|
+
# Every request needs a token; a dev key makes local runs work without an issuer.
|
|
1406
|
+
console.print(
|
|
1407
|
+
' [bold bright_green]export GRAPH_AGENTS_CLI_API_KEY="$(graph-agents-cli auth '
|
|
1408
|
+
'dev-token --sub alice)"[/]',
|
|
1409
|
+
highlight=False,
|
|
1410
|
+
)
|
|
1411
|
+
console.print(" [bold bright_green]graph-agents-cli playground[/]")
|
|
1412
|
+
console.print(" [bold bright_green]graph-agents-cli eval run[/]")
|
|
1413
|
+
if params.deployment_target == "kubernetes":
|
|
1414
|
+
console.print(" [bold bright_green]graph-agents-cli deploy --env dev[/]")
|
|
1415
|
+
if params.registry == DEFAULT_REGISTRY_PLACEHOLDER:
|
|
1416
|
+
console.print(
|
|
1417
|
+
f"\n [yellow]Before `deploy`: the registry is still the placeholder "
|
|
1418
|
+
f"'{DEFAULT_REGISTRY_PLACEHOLDER}'. Run `{REGISTRY_FIX_COMMAND}` "
|
|
1419
|
+
f"({REGISTRY_FIX_EFFECT}).[/]"
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
# Kept importable for callers that referenced the old constant name.
|
|
1424
|
+
GUIDANCE_FILENAME_DEFAULT = DEFAULT_AGENT_GUIDANCE_FILENAME
|