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
graph_agents_cli/scaffold/deployment_targets/kubernetes/python/tests/integration/test_chart.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Copyright 2026 graph-agents-cli contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Lint and render the Helm chart when `helm` is on PATH (skipped otherwise).
|
|
16
|
+
|
|
17
|
+
The chart declares optional subcharts; `helm dependency build` needs the
|
|
18
|
+
chart registry, so it is attempted once and the test skips when it cannot run.
|
|
19
|
+
|
|
20
|
+
The values files are yours: every expectation below is read from them
|
|
21
|
+
(values.yaml overlaid with values-<env>.yaml, as helm merges them), so turning
|
|
22
|
+
the gateway off, an ingress on or the bundled Postgres off keeps these tests
|
|
23
|
+
green as long as the chart renders what the values ask for. They also hold
|
|
24
|
+
whatever image tags the values files carry: in argocd mode CI and
|
|
25
|
+
`graph-agents-cli deploy` commit the tag to values-<env>.yaml, and the pull
|
|
26
|
+
request that does so runs them too. What stays fixed is the security posture:
|
|
27
|
+
probes and metrics are never published, pods are hardened.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import shutil
|
|
33
|
+
import subprocess
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
import pytest
|
|
37
|
+
import yaml
|
|
38
|
+
|
|
39
|
+
PROJECT_NAME = "{{cookiecutter.project_name}}"
|
|
40
|
+
CHART = Path(__file__).resolve().parents[2] / "deployment" / "helm" / PROJECT_NAME
|
|
41
|
+
ENVIRONMENTS = ("dev", "staging", "prod")
|
|
42
|
+
HELM = shutil.which("helm")
|
|
43
|
+
# The Gateway the scaffolded staging/prod values leave for the operator to name.
|
|
44
|
+
GATEWAY = ("--set", "gateway.parentRef.name=gw")
|
|
45
|
+
# What `graph-agents-cli deploy` passes: the image tag it deploys (always as a
|
|
46
|
+
# string), and the Gateway.
|
|
47
|
+
DEPLOY_ARGS = ("--set-string", "image.tag=0123abc", *GATEWAY)
|
|
48
|
+
|
|
49
|
+
pytestmark = pytest.mark.skipif(HELM is None, reason="helm is not installed")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _helm(*args: str) -> subprocess.CompletedProcess[str]:
|
|
53
|
+
return subprocess.run([HELM or "helm", *args], capture_output=True, text=True, check=False)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _render(env: str, *extra: str) -> subprocess.CompletedProcess[str]:
|
|
57
|
+
return _helm(
|
|
58
|
+
"template",
|
|
59
|
+
PROJECT_NAME,
|
|
60
|
+
str(CHART),
|
|
61
|
+
"-f",
|
|
62
|
+
str(CHART / f"values-{env}.yaml"),
|
|
63
|
+
"--namespace",
|
|
64
|
+
f"{PROJECT_NAME}-{env}",
|
|
65
|
+
*extra,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _docs(manifests: str) -> list[dict]:
|
|
70
|
+
return [doc for doc in yaml.safe_load_all(manifests) if doc]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _agent_deployment(manifests: str) -> dict:
|
|
74
|
+
for doc in _docs(manifests):
|
|
75
|
+
if doc["kind"] == "Deployment" and doc["metadata"]["name"] == PROJECT_NAME:
|
|
76
|
+
return doc
|
|
77
|
+
raise AssertionError("the agent Deployment was not rendered")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _committed_tag(env: str) -> str:
|
|
81
|
+
values = yaml.safe_load((CHART / f"values-{env}.yaml").read_text()) or {}
|
|
82
|
+
return str((values.get("image") or {}).get("tag") or "")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _merge(base: dict, override: dict) -> dict:
|
|
86
|
+
"""Helm's values merge: maps merge key by key, anything else is replaced."""
|
|
87
|
+
merged = dict(base)
|
|
88
|
+
for key, value in override.items():
|
|
89
|
+
if isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
90
|
+
merged[key] = _merge(merged[key], value)
|
|
91
|
+
else:
|
|
92
|
+
merged[key] = value
|
|
93
|
+
return merged
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _values(env: str) -> dict:
|
|
97
|
+
"""The values `helm template -f values-<env>.yaml` renders with."""
|
|
98
|
+
base = yaml.safe_load((CHART / "values.yaml").read_text()) or {}
|
|
99
|
+
return _merge(base, yaml.safe_load((CHART / f"values-{env}.yaml").read_text()) or {})
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _enabled(values: dict, key: str) -> bool:
|
|
103
|
+
return bool((values.get(key) or {}).get("enabled"))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _published_paths(manifests: str) -> dict[str, str]:
|
|
107
|
+
"""Every path the HTTPRoute and the Ingress publish: path -> match type."""
|
|
108
|
+
paths: dict[str, str] = {}
|
|
109
|
+
for doc in _docs(manifests):
|
|
110
|
+
if doc["kind"] == "HTTPRoute":
|
|
111
|
+
for rule in doc["spec"]["rules"]:
|
|
112
|
+
for match in rule["matches"]:
|
|
113
|
+
paths[match["path"]["value"]] = match["path"]["type"]
|
|
114
|
+
elif doc["kind"] == "Ingress":
|
|
115
|
+
for rule in doc["spec"]["rules"]:
|
|
116
|
+
for entry in rule["http"]["paths"]:
|
|
117
|
+
kind = "Exact" if entry["pathType"] == "Exact" else "PathPrefix"
|
|
118
|
+
paths[entry["path"]] = kind
|
|
119
|
+
return paths
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _configured_paths(values: dict) -> dict[str, str]:
|
|
123
|
+
"""route.publicPaths, plus route.devPaths while env.APP_ENV is exactly dev (as the chart does)."""
|
|
124
|
+
route = values.get("route") or {}
|
|
125
|
+
entries = list(route.get("publicPaths") or [])
|
|
126
|
+
if str((values.get("env") or {}).get("APP_ENV", "")) == "dev":
|
|
127
|
+
entries += list(route.get("devPaths") or [])
|
|
128
|
+
return {str(e["path"]): str(e["type"]) for e in entries}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@pytest.fixture(scope="module")
|
|
132
|
+
def chart_with_dependencies() -> Path:
|
|
133
|
+
if not (CHART / "charts").exists():
|
|
134
|
+
result = _helm("dependency", "build", str(CHART))
|
|
135
|
+
if result.returncode != 0:
|
|
136
|
+
pytest.skip(
|
|
137
|
+
f"helm dependency build failed (no registry access?): {result.stderr[-400:]}"
|
|
138
|
+
)
|
|
139
|
+
return CHART
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_helm_lint(chart_with_dependencies: Path) -> None:
|
|
143
|
+
for env in ENVIRONMENTS:
|
|
144
|
+
result = _helm(
|
|
145
|
+
"lint",
|
|
146
|
+
str(chart_with_dependencies),
|
|
147
|
+
"-f",
|
|
148
|
+
str(chart_with_dependencies / f"values-{env}.yaml"),
|
|
149
|
+
*DEPLOY_ARGS,
|
|
150
|
+
)
|
|
151
|
+
assert result.returncode == 0, f"{env}: {result.stdout}\n{result.stderr}"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_helm_template_renders_every_environment(chart_with_dependencies: Path) -> None:
|
|
155
|
+
for env in ENVIRONMENTS:
|
|
156
|
+
values = _values(env)
|
|
157
|
+
result = _render(env, *DEPLOY_ARGS)
|
|
158
|
+
assert result.returncode == 0, f"{env}: {result.stderr}"
|
|
159
|
+
out = result.stdout
|
|
160
|
+
assert "kind: Deployment" in out and "kind: Service" in out and "kind: ConfigMap" in out
|
|
161
|
+
kinds = {doc["kind"] for doc in _docs(out)}
|
|
162
|
+
container = _agent_deployment(out)["spec"]["template"]["spec"]["containers"][0]
|
|
163
|
+
secret_ref = container["envFrom"][0]["secretRef"]
|
|
164
|
+
assert secret_ref["name"] == f"{PROJECT_NAME}-app"
|
|
165
|
+
# secretOptional: false (the default) keeps pods without their keys from starting.
|
|
166
|
+
assert secret_ref["optional"] is bool(values.get("secretOptional", False)), env
|
|
167
|
+
assert ("HTTPRoute" in kinds) is _enabled(values, "gateway"), env
|
|
168
|
+
assert ("Ingress" in kinds) is _enabled(values, "ingress"), env
|
|
169
|
+
bundled = f"$(POSTGRES_PASSWORD)@{PROJECT_NAME}-postgresql" in out
|
|
170
|
+
assert bundled is _enabled(values, "postgresql"), env
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_the_route_publishes_the_api_but_not_probes_or_metrics(
|
|
174
|
+
chart_with_dependencies: Path,
|
|
175
|
+
) -> None:
|
|
176
|
+
routed = [
|
|
177
|
+
env
|
|
178
|
+
for env in ENVIRONMENTS
|
|
179
|
+
if _enabled(_values(env), "gateway") or _enabled(_values(env), "ingress")
|
|
180
|
+
]
|
|
181
|
+
if not routed:
|
|
182
|
+
pytest.skip("no environment enables gateway or ingress")
|
|
183
|
+
for env in routed:
|
|
184
|
+
values = _values(env)
|
|
185
|
+
result = _render(env, *DEPLOY_ARGS)
|
|
186
|
+
assert result.returncode == 0, f"{env}: {result.stderr}"
|
|
187
|
+
paths = _published_paths(result.stdout)
|
|
188
|
+
# Exactly what the values list: nothing else reaches the Gateway or Ingress.
|
|
189
|
+
assert paths == _configured_paths(values), env
|
|
190
|
+
for private in ("/", "/health", "/ready", "/metrics"):
|
|
191
|
+
assert private not in paths, (env, private)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_pods_are_hardened_probed_and_sized(chart_with_dependencies: Path) -> None:
|
|
195
|
+
for env in ENVIRONMENTS:
|
|
196
|
+
result = _render(env, *DEPLOY_ARGS)
|
|
197
|
+
assert result.returncode == 0, f"{env}: {result.stderr}"
|
|
198
|
+
pod = _agent_deployment(result.stdout)["spec"]["template"]["spec"]
|
|
199
|
+
container = pod["containers"][0]
|
|
200
|
+
assert container["image"].endswith(":0123abc")
|
|
201
|
+
assert pod["automountServiceAccountToken"] is False
|
|
202
|
+
assert pod["securityContext"]["runAsNonRoot"] is True
|
|
203
|
+
assert pod["securityContext"]["seccompProfile"]["type"] == "RuntimeDefault"
|
|
204
|
+
security = container["securityContext"]
|
|
205
|
+
assert security["readOnlyRootFilesystem"] is True
|
|
206
|
+
assert security["allowPrivilegeEscalation"] is False
|
|
207
|
+
assert security["capabilities"]["drop"] == ["ALL"]
|
|
208
|
+
assert {"name": "tmp", "mountPath": "/tmp"} in container["volumeMounts"]
|
|
209
|
+
assert container["readinessProbe"]["httpGet"]["path"] == "/ready"
|
|
210
|
+
assert container["livenessProbe"]["httpGet"]["path"] == "/health"
|
|
211
|
+
assert container["startupProbe"]["httpGet"]["path"] == "/health"
|
|
212
|
+
assert container["resources"]["requests"]["cpu"]
|
|
213
|
+
assert container["resources"]["limits"]["memory"]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_pods_drain_before_they_are_killed(chart_with_dependencies: Path) -> None:
|
|
217
|
+
"""A stopping pod keeps serving during the preStop pause and drains within the grace period."""
|
|
218
|
+
for env in ENVIRONMENTS:
|
|
219
|
+
result = _render(env, *DEPLOY_ARGS)
|
|
220
|
+
assert result.returncode == 0, f"{env}: {result.stderr}"
|
|
221
|
+
pod = _agent_deployment(result.stdout)["spec"]["template"]["spec"]
|
|
222
|
+
container = pod["containers"][0]
|
|
223
|
+
values = yaml.safe_load((CHART / "values.yaml").read_text()) or {}
|
|
224
|
+
overrides = yaml.safe_load((CHART / f"values-{env}.yaml").read_text()) or {}
|
|
225
|
+
shutdown = {**(values.get("shutdown") or {}), **(overrides.get("shutdown") or {})}
|
|
226
|
+
pause = int(shutdown.get("preStopSleepSeconds") or 0)
|
|
227
|
+
drain = int(shutdown.get("drainSeconds") or 0)
|
|
228
|
+
assert pod["terminationGracePeriodSeconds"] > pause + drain, env
|
|
229
|
+
if pause:
|
|
230
|
+
command = container["lifecycle"]["preStop"]["exec"]["command"]
|
|
231
|
+
assert command[-1] == f"sleep {pause}", env
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_the_network_policy_example_renders_a_valid_policy(chart_with_dependencies: Path) -> None:
|
|
235
|
+
"""examples/networkpolicy.yaml, as documented: copied over the staging or prod values."""
|
|
236
|
+
example = CHART / "examples" / "networkpolicy.yaml"
|
|
237
|
+
if not example.is_file():
|
|
238
|
+
pytest.skip("no examples/networkpolicy.yaml")
|
|
239
|
+
for env in ("staging", "prod"):
|
|
240
|
+
result = _render(env, "-f", str(example), *DEPLOY_ARGS)
|
|
241
|
+
assert result.returncode == 0, f"{env}: {result.stderr}"
|
|
242
|
+
policy = next(d for d in _docs(result.stdout) if d["kind"] == "NetworkPolicy")
|
|
243
|
+
spec = policy["spec"]
|
|
244
|
+
assert spec["policyTypes"] == ["Ingress", "Egress"], env
|
|
245
|
+
assert spec["ingress"] and all(rule.get("ports") for rule in spec["ingress"]), env
|
|
246
|
+
# Every egress rule names its ports: nothing is open to any port anywhere.
|
|
247
|
+
assert all(rule.get("ports") for rule in spec["egress"]), env
|
|
248
|
+
dns = {p["port"] for p in spec["egress"][0]["ports"]}
|
|
249
|
+
assert dns == {53}, env
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_the_chart_refuses_to_render_without_an_image_tag(chart_with_dependencies: Path) -> None:
|
|
253
|
+
# An explicit empty tag: the committed values-<env>.yaml may already name one.
|
|
254
|
+
for env in ENVIRONMENTS:
|
|
255
|
+
result = _render(env, "--set-string", "image.tag=", *GATEWAY)
|
|
256
|
+
assert result.returncode != 0, env
|
|
257
|
+
assert "image.tag is empty" in result.stderr, env
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_committed_image_tags_are_the_ones_deployed(chart_with_dependencies: Path) -> None:
|
|
261
|
+
"""A tag written into values-<env>.yaml (argocd mode) renders as Argo CD renders it."""
|
|
262
|
+
tagged = {env: tag for env in ENVIRONMENTS if (tag := _committed_tag(env))}
|
|
263
|
+
if not tagged:
|
|
264
|
+
pytest.skip("no values-<env>.yaml names an image tag yet")
|
|
265
|
+
for env, tag in tagged.items():
|
|
266
|
+
result = _render(env, *GATEWAY)
|
|
267
|
+
assert result.returncode == 0, f"{env}: {result.stderr}"
|
|
268
|
+
container = _agent_deployment(result.stdout)["spec"]["template"]["spec"]["containers"][0]
|
|
269
|
+
assert container["image"].endswith(f":{tag}"), (env, container["image"])
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# {{cookiecutter.project_name}}
|
|
2
|
+
|
|
3
|
+
Created by graph-agents-cli with `--deployment-target none`: no `deployment/`
|
|
4
|
+
directory, no CD workflows. Run it locally with `graph-agents-cli playground`
|
|
5
|
+
or `graph-agents-cli run "prompt"`; add a Kubernetes target later with
|
|
6
|
+
`graph-agents-cli scaffold enhance --deployment-target kubernetes`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Shared backup utility for project directories.
|
|
16
|
+
|
|
17
|
+
A backup is the undo of ``enhance`` and ``upgrade``, so it copies the whole
|
|
18
|
+
project, ``.env`` included (both commands merge it, and a generated project
|
|
19
|
+
gitignores it, so the backup may hold the user's only copy). Because of that it
|
|
20
|
+
is private: the backups directory and every backup are 0700 and every ``.env*``
|
|
21
|
+
file in them 0600. Only the newest ``KEEP_BACKUPS`` backups of a project are
|
|
22
|
+
kept, so credentials do not pile up in the home directory.
|
|
23
|
+
|
|
24
|
+
A backup is named ``<directory>_<project id>_<timestamp>[_n]``. The project id
|
|
25
|
+
is a hash of the project's resolved absolute path and its manifest ``name``, so
|
|
26
|
+
two checkouts with the same directory name (``my-agent`` in two places) never
|
|
27
|
+
count as one project: pruning only ever deletes backups carrying the id of the
|
|
28
|
+
project being backed up. Backups named without an id (made by an older CLI)
|
|
29
|
+
are never pruned.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import contextlib
|
|
33
|
+
import datetime
|
|
34
|
+
import hashlib
|
|
35
|
+
import os
|
|
36
|
+
import pathlib
|
|
37
|
+
import re
|
|
38
|
+
import shutil
|
|
39
|
+
from collections.abc import Callable
|
|
40
|
+
|
|
41
|
+
import click
|
|
42
|
+
import yaml
|
|
43
|
+
|
|
44
|
+
from graph_agents_cli._output import Console
|
|
45
|
+
|
|
46
|
+
from .fs import standard_ignore_patterns
|
|
47
|
+
|
|
48
|
+
BACKUP_BASE_DIR = pathlib.Path.home() / ".graph-agents-cli" / "backups"
|
|
49
|
+
# Backups kept per project (by project id); older ones are deleted.
|
|
50
|
+
KEEP_BACKUPS = 5
|
|
51
|
+
_PRIVATE_DIR = 0o700
|
|
52
|
+
_PRIVATE_FILE = 0o600
|
|
53
|
+
_MANIFEST_FILENAME = "graph-agents-cli-manifest.yaml"
|
|
54
|
+
_ID_LENGTH = 12
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def project_backup_id(project_dir: pathlib.Path) -> str:
|
|
58
|
+
"""A stable id for ``project_dir``: its resolved absolute path plus its manifest ``name``."""
|
|
59
|
+
name = ""
|
|
60
|
+
try:
|
|
61
|
+
data = yaml.safe_load((project_dir / _MANIFEST_FILENAME).read_text(encoding="utf-8"))
|
|
62
|
+
if isinstance(data, dict) and data.get("name") is not None:
|
|
63
|
+
name = str(data["name"])
|
|
64
|
+
except (OSError, UnicodeDecodeError, yaml.YAMLError):
|
|
65
|
+
pass # no (readable) manifest: the path alone identifies the directory
|
|
66
|
+
key = f"{project_dir.resolve()}\0{name}".encode()
|
|
67
|
+
return hashlib.sha256(key).hexdigest()[:_ID_LENGTH]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _backup_prefix(project_dir: pathlib.Path) -> str:
|
|
71
|
+
return f"{project_dir.name}_{project_backup_id(project_dir)}"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def create_project_backup(
|
|
75
|
+
project_dir: pathlib.Path,
|
|
76
|
+
console: Console | None = None,
|
|
77
|
+
auto_approve: bool = False,
|
|
78
|
+
interactive: bool = False,
|
|
79
|
+
) -> pathlib.Path | None:
|
|
80
|
+
"""Create a backup of the project directory.
|
|
81
|
+
|
|
82
|
+
Backs up to ~/.graph-agents-cli/backups/<directory>_<project id>_<timestamp>/.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
project_dir: Path to the project directory to back up.
|
|
86
|
+
console: Rich console for output. Created if not provided.
|
|
87
|
+
auto_approve: If True, skip confirmation prompts on failure.
|
|
88
|
+
interactive: If True, show interactive prompt on backup failure.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Path to the backup directory on success, None if backup failed
|
|
92
|
+
but user chose to continue.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
click.Abort: If backup failed and user chose to cancel (interactive mode only).
|
|
96
|
+
"""
|
|
97
|
+
if console is None:
|
|
98
|
+
console = Console()
|
|
99
|
+
|
|
100
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
101
|
+
prefix = _backup_prefix(project_dir)
|
|
102
|
+
backup_dir = BACKUP_BASE_DIR / f"{prefix}_{timestamp}"
|
|
103
|
+
suffix = 1
|
|
104
|
+
while backup_dir.exists(): # two backups within the same second
|
|
105
|
+
suffix += 1
|
|
106
|
+
backup_dir = BACKUP_BASE_DIR / f"{prefix}_{timestamp}_{suffix}"
|
|
107
|
+
|
|
108
|
+
console.print("📦 [blue]Creating backup before modification...[/blue]")
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
BACKUP_BASE_DIR.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
_make_private_dir(BACKUP_BASE_DIR)
|
|
113
|
+
shutil.copytree(project_dir, backup_dir, ignore=standard_ignore_patterns)
|
|
114
|
+
_make_private_tree(backup_dir)
|
|
115
|
+
console.print(f"Backup created: [cyan]{backup_dir}[/cyan]")
|
|
116
|
+
pruned = prune_backups(project_dir, keep=KEEP_BACKUPS, current=backup_dir)
|
|
117
|
+
if pruned:
|
|
118
|
+
console.print(
|
|
119
|
+
f"[dim]Removed {len(pruned)} older backup(s) of {project_dir.name} "
|
|
120
|
+
f"(the newest {KEEP_BACKUPS} are kept).[/dim]"
|
|
121
|
+
)
|
|
122
|
+
return backup_dir
|
|
123
|
+
except Exception as e:
|
|
124
|
+
console.print(f"⚠️ [yellow]Warning: Could not create backup: {e}[/yellow]")
|
|
125
|
+
if interactive:
|
|
126
|
+
if not click.confirm("Continue without backup?", default=True):
|
|
127
|
+
raise click.Abort() from e
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _make_private_dir(path: pathlib.Path) -> None:
|
|
132
|
+
with contextlib.suppress(OSError):
|
|
133
|
+
os.chmod(path, _PRIVATE_DIR)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _make_private_tree(root: pathlib.Path) -> None:
|
|
137
|
+
"""0700 on the backup and its directories, 0600 on the env files it holds."""
|
|
138
|
+
_make_private_dir(root)
|
|
139
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
140
|
+
for name in dirnames:
|
|
141
|
+
_make_private_dir(pathlib.Path(dirpath) / name)
|
|
142
|
+
for name in filenames:
|
|
143
|
+
if name.startswith(".env"):
|
|
144
|
+
with contextlib.suppress(OSError):
|
|
145
|
+
os.chmod(pathlib.Path(dirpath) / name, _PRIVATE_FILE)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def prune_backups(
|
|
149
|
+
project_dir: pathlib.Path, *, keep: int = KEEP_BACKUPS, current: pathlib.Path | None = None
|
|
150
|
+
) -> list[pathlib.Path]:
|
|
151
|
+
"""Delete all but the newest ``keep`` backups of ``project_dir``; return what was removed.
|
|
152
|
+
|
|
153
|
+
Only directories named exactly ``<directory>_<project id>_<YYYYmmdd>_<HHMMSS>[_n]``
|
|
154
|
+
with this project's id are considered: another project is never touched,
|
|
155
|
+
whether its name shares a prefix or is the same (another checkout), backups
|
|
156
|
+
without an id are left alone, and ``current`` (the backup just made) is
|
|
157
|
+
never removed.
|
|
158
|
+
"""
|
|
159
|
+
if not BACKUP_BASE_DIR.is_dir():
|
|
160
|
+
return []
|
|
161
|
+
prefix = _backup_prefix(project_dir)
|
|
162
|
+
pattern = re.compile(rf"^{re.escape(prefix)}_(\d{{8}}_\d{{6}})(?:_(\d+))?$")
|
|
163
|
+
found: list[tuple[str, int, pathlib.Path]] = []
|
|
164
|
+
for entry in BACKUP_BASE_DIR.iterdir():
|
|
165
|
+
match = pattern.match(entry.name)
|
|
166
|
+
if match and entry.is_dir() and not entry.is_symlink():
|
|
167
|
+
found.append((match.group(1), int(match.group(2) or 1), entry))
|
|
168
|
+
found.sort(reverse=True)
|
|
169
|
+
removed: list[pathlib.Path] = []
|
|
170
|
+
for _stamp, _n, entry in found[max(keep, 1) :]:
|
|
171
|
+
if current is not None and entry == current:
|
|
172
|
+
continue
|
|
173
|
+
shutil.rmtree(entry, ignore_errors=True)
|
|
174
|
+
removed.append(entry)
|
|
175
|
+
return removed
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def make_backup_pre_apply_hook(
|
|
179
|
+
*,
|
|
180
|
+
console: Console,
|
|
181
|
+
auto_approve: bool,
|
|
182
|
+
interactive: bool,
|
|
183
|
+
) -> Callable[[pathlib.Path], bool]:
|
|
184
|
+
"""Build a ``run_three_way_merge`` pre-apply hook that backs up the project.
|
|
185
|
+
|
|
186
|
+
The merge invokes the hook right before it writes changes; returning False
|
|
187
|
+
aborts the merge. This wraps ``create_project_backup`` so a user cancelling
|
|
188
|
+
the "continue without backup?" prompt cleanly stops the operation.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
console: Rich console for output.
|
|
192
|
+
auto_approve: If True, skip confirmation prompts on failure.
|
|
193
|
+
interactive: If True, prompt the user on backup failure.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
A hook taking the project directory and returning True to proceed,
|
|
197
|
+
False if the user cancelled.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def _backup(proj_dir: pathlib.Path) -> bool:
|
|
201
|
+
try:
|
|
202
|
+
create_project_backup(
|
|
203
|
+
proj_dir,
|
|
204
|
+
console=console,
|
|
205
|
+
auto_approve=auto_approve,
|
|
206
|
+
interactive=interactive,
|
|
207
|
+
)
|
|
208
|
+
return True
|
|
209
|
+
except click.Abort:
|
|
210
|
+
return False # user cancelled
|
|
211
|
+
|
|
212
|
+
return _backup
|