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,264 @@
|
|
|
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
|
+
"""A project's response schema (structured final answers): the check the CLI applies.
|
|
16
|
+
|
|
17
|
+
A project that declares `<agent directory>/response_schema.json` answers in
|
|
18
|
+
JSON of that shape (the template's `app_utils/structured.py`). The schema may
|
|
19
|
+
use only the JSON Schema subset the runtime checks answers with, and its root
|
|
20
|
+
is an object; `create --response-schema` refuses to seed any other file, and
|
|
21
|
+
`lint` reports one. The rules are the template's own (the SHARED block below,
|
|
22
|
+
byte-identical there), so a schema the CLI accepts starts the agent.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import math
|
|
29
|
+
import re
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
import click
|
|
34
|
+
|
|
35
|
+
SCHEMA_FILENAME = "response_schema.json"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ResponseSchemaFileError(click.ClickException):
|
|
39
|
+
"""A response schema that cannot be read or uses what the answer check cannot check (exit 3)."""
|
|
40
|
+
|
|
41
|
+
exit_code = 3
|
|
42
|
+
|
|
43
|
+
def __init__(self, path: str | Path, problems: list[str]) -> None:
|
|
44
|
+
self.path = Path(path)
|
|
45
|
+
self.problems = list(problems)
|
|
46
|
+
lines = "\n".join(f" - {p}" for p in self.problems)
|
|
47
|
+
super().__init__(f"Invalid response schema {self.path}:\n{lines}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# --- BEGIN SHARED RESPONSE SCHEMA RULES ---
|
|
51
|
+
# The JSON Schema subset a response schema may use, and the check of a schema
|
|
52
|
+
# against it. The same block is in graph_agents_cli/_response_schema.py, which
|
|
53
|
+
# `create --response-schema` and `lint` use: keep the two byte-identical
|
|
54
|
+
# (tests/dev/test_response_schema_parity.py), so a schema the CLI accepts
|
|
55
|
+
# starts the agent. No Jinja, no imports but json, math, re and typing.Any.
|
|
56
|
+
|
|
57
|
+
# Keywords that are checked, and annotations that are allowed and ignored.
|
|
58
|
+
VALIDATION_KEYWORDS = frozenset(
|
|
59
|
+
{
|
|
60
|
+
"type",
|
|
61
|
+
"enum",
|
|
62
|
+
"const",
|
|
63
|
+
"properties",
|
|
64
|
+
"required",
|
|
65
|
+
"additionalProperties",
|
|
66
|
+
"minProperties",
|
|
67
|
+
"maxProperties",
|
|
68
|
+
"items",
|
|
69
|
+
"minItems",
|
|
70
|
+
"maxItems",
|
|
71
|
+
"uniqueItems",
|
|
72
|
+
"minLength",
|
|
73
|
+
"maxLength",
|
|
74
|
+
"pattern",
|
|
75
|
+
"minimum",
|
|
76
|
+
"maximum",
|
|
77
|
+
"exclusiveMinimum",
|
|
78
|
+
"exclusiveMaximum",
|
|
79
|
+
"multipleOf",
|
|
80
|
+
"anyOf",
|
|
81
|
+
"oneOf",
|
|
82
|
+
"allOf",
|
|
83
|
+
"not",
|
|
84
|
+
"$ref",
|
|
85
|
+
"$defs",
|
|
86
|
+
"definitions",
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
ANNOTATION_KEYWORDS = frozenset(
|
|
90
|
+
{
|
|
91
|
+
"$schema",
|
|
92
|
+
"$id",
|
|
93
|
+
"$comment",
|
|
94
|
+
"title",
|
|
95
|
+
"description",
|
|
96
|
+
"default",
|
|
97
|
+
"examples",
|
|
98
|
+
"deprecated",
|
|
99
|
+
"readOnly",
|
|
100
|
+
"writeOnly",
|
|
101
|
+
"format",
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
SUPPORTED_KEYWORDS = VALIDATION_KEYWORDS | ANNOTATION_KEYWORDS
|
|
105
|
+
JSON_TYPES = frozenset({"object", "array", "string", "integer", "number", "boolean", "null"})
|
|
106
|
+
_SCHEMA_LISTS = ("anyOf", "oneOf", "allOf")
|
|
107
|
+
_SCHEMA_MAPS = ("properties", "$defs", "definitions")
|
|
108
|
+
_SCHEMA_VALUES = ("items", "additionalProperties", "not")
|
|
109
|
+
_COUNTS = ("minProperties", "maxProperties", "minItems", "maxItems", "minLength", "maxLength")
|
|
110
|
+
_NUMBERS = ("minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _pointer(schema: Any, ref: str) -> Any:
|
|
114
|
+
"""The subschema a local `$ref` (`#`, `#/$defs/name`, ...) names; KeyError when none."""
|
|
115
|
+
if ref == "#":
|
|
116
|
+
return schema
|
|
117
|
+
if not ref.startswith("#/"):
|
|
118
|
+
raise KeyError(ref)
|
|
119
|
+
node = schema
|
|
120
|
+
for raw in ref[2:].split("/"):
|
|
121
|
+
key = raw.replace("~1", "/").replace("~0", "~")
|
|
122
|
+
if isinstance(node, dict) and key in node:
|
|
123
|
+
node = node[key]
|
|
124
|
+
elif isinstance(node, list) and key.isdigit() and int(key) < len(node):
|
|
125
|
+
node = node[int(key)]
|
|
126
|
+
else:
|
|
127
|
+
raise KeyError(ref)
|
|
128
|
+
return node
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _is_number(value: Any) -> bool:
|
|
132
|
+
return isinstance(value, int | float) and not isinstance(value, bool) and math.isfinite(value)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def schema_problems(schema: Any) -> list[str]:
|
|
136
|
+
"""What stops `schema` from being a response schema; empty when it can be one.
|
|
137
|
+
|
|
138
|
+
The root is an object schema (`"type": "object"`); every keyword anywhere
|
|
139
|
+
is one `SUPPORTED_KEYWORDS` lists, with a value of the right kind; every
|
|
140
|
+
`$ref` points inside the document; every `pattern` compiles.
|
|
141
|
+
"""
|
|
142
|
+
if not isinstance(schema, dict):
|
|
143
|
+
return ["the schema must be a JSON object"]
|
|
144
|
+
problems: list[str] = []
|
|
145
|
+
if schema.get("type") != "object":
|
|
146
|
+
problems.append('$: the root must be an object schema ("type": "object")')
|
|
147
|
+
|
|
148
|
+
def walk(node: Any, where: str) -> None:
|
|
149
|
+
if isinstance(node, bool):
|
|
150
|
+
return
|
|
151
|
+
if not isinstance(node, dict):
|
|
152
|
+
problems.append(f"{where}: a schema must be an object or a boolean")
|
|
153
|
+
return
|
|
154
|
+
for key, value in node.items():
|
|
155
|
+
at = f"{where}.{key}"
|
|
156
|
+
if key not in SUPPORTED_KEYWORDS:
|
|
157
|
+
problems.append(f"{at}: `{key}` is not a keyword the answer check supports")
|
|
158
|
+
elif key == "type":
|
|
159
|
+
types = value if isinstance(value, list) else [value]
|
|
160
|
+
if not types or not all(t in JSON_TYPES for t in types):
|
|
161
|
+
problems.append(f"{at}: must name JSON types ({', '.join(sorted(JSON_TYPES))})")
|
|
162
|
+
elif key == "required":
|
|
163
|
+
if not isinstance(value, list) or not all(isinstance(v, str) for v in value):
|
|
164
|
+
problems.append(f"{at}: must be a list of property names")
|
|
165
|
+
elif key == "enum":
|
|
166
|
+
if not isinstance(value, list) or not value:
|
|
167
|
+
problems.append(f"{at}: must be a non-empty list")
|
|
168
|
+
elif key in _COUNTS:
|
|
169
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
170
|
+
problems.append(f"{at}: must be a whole number >= 0")
|
|
171
|
+
elif key in _NUMBERS:
|
|
172
|
+
if not _is_number(value) or (key == "multipleOf" and value <= 0):
|
|
173
|
+
problems.append(
|
|
174
|
+
f"{at}: must be a number" + (" > 0" if key == "multipleOf" else "")
|
|
175
|
+
)
|
|
176
|
+
elif key == "uniqueItems":
|
|
177
|
+
if not isinstance(value, bool):
|
|
178
|
+
problems.append(f"{at}: must be true or false")
|
|
179
|
+
elif key == "pattern":
|
|
180
|
+
try:
|
|
181
|
+
re.compile(value)
|
|
182
|
+
except (re.error, TypeError) as exc:
|
|
183
|
+
problems.append(f"{at}: not a regular expression ({exc})")
|
|
184
|
+
elif key == "$ref":
|
|
185
|
+
try:
|
|
186
|
+
_pointer(schema, value if isinstance(value, str) else "")
|
|
187
|
+
except KeyError:
|
|
188
|
+
problems.append(f"{at}: {value!r} does not point inside this schema")
|
|
189
|
+
elif key in _SCHEMA_LISTS:
|
|
190
|
+
if not isinstance(value, list) or not value:
|
|
191
|
+
problems.append(f"{at}: must be a non-empty list of schemas")
|
|
192
|
+
else:
|
|
193
|
+
for i, sub in enumerate(value):
|
|
194
|
+
walk(sub, f"{at}[{i}]")
|
|
195
|
+
elif key in _SCHEMA_MAPS:
|
|
196
|
+
if not isinstance(value, dict):
|
|
197
|
+
problems.append(f"{at}: must map names to schemas")
|
|
198
|
+
else:
|
|
199
|
+
for name, sub in value.items():
|
|
200
|
+
walk(sub, f"{at}.{name}")
|
|
201
|
+
elif key in _SCHEMA_VALUES:
|
|
202
|
+
if key == "items" and isinstance(value, list):
|
|
203
|
+
problems.append(
|
|
204
|
+
f"{at}: must be one schema (a list of schemas is not supported)"
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
walk(value, at)
|
|
208
|
+
|
|
209
|
+
walk(schema, "$")
|
|
210
|
+
return problems
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# --- END SHARED RESPONSE SCHEMA RULES ---
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def read_schema(path: Path) -> tuple[dict[str, Any] | None, list[str]]:
|
|
217
|
+
"""``(schema, [])`` for a file that can be the response schema, else ``(None, problems)``."""
|
|
218
|
+
try:
|
|
219
|
+
schema = json.loads(path.read_text(encoding="utf-8"))
|
|
220
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
221
|
+
return None, [f"not a JSON file ({exc})"]
|
|
222
|
+
problems = schema_problems(schema)
|
|
223
|
+
return (None, problems) if problems else (schema, [])
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def load_schema(path: str | Path) -> dict[str, Any]:
|
|
227
|
+
"""The schema in ``path``; ``ResponseSchemaFileError`` listing every problem."""
|
|
228
|
+
schema, problems = read_schema(Path(path))
|
|
229
|
+
if schema is None:
|
|
230
|
+
raise ResponseSchemaFileError(path, problems)
|
|
231
|
+
return schema
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def schema_file(project_root: Path, agent_directory: str) -> Path:
|
|
235
|
+
"""Where a project keeps its response schema."""
|
|
236
|
+
return project_root / agent_directory / SCHEMA_FILENAME
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def wiring_problems(project_root: Path, agent_directory: str) -> list[str]:
|
|
240
|
+
"""What the project's ``agent.py`` lacks to answer in its response schema; empty when wired.
|
|
241
|
+
|
|
242
|
+
It builds the agent with ``response_format=response_format(model, tools)`` (else
|
|
243
|
+
every run ends with ``invalid_structured_response``) and has ``StructuredAnswer()``
|
|
244
|
+
in its middleware (else an answer that does not fit is not sent back to the model,
|
|
245
|
+
and the run ends with that error instead of the answer).
|
|
246
|
+
"""
|
|
247
|
+
agent_py = f"{agent_directory}/agent.py"
|
|
248
|
+
try:
|
|
249
|
+
text = (project_root / agent_directory / "agent.py").read_text(encoding="utf-8")
|
|
250
|
+
except OSError:
|
|
251
|
+
return [f"{agent_py} cannot be read"]
|
|
252
|
+
problems = []
|
|
253
|
+
if "response_format(" not in text:
|
|
254
|
+
problems.append(
|
|
255
|
+
f"{agent_py} does not pass response_format=response_format(model, tools) to "
|
|
256
|
+
"create_agent: every run would end with invalid_structured_response"
|
|
257
|
+
)
|
|
258
|
+
if "StructuredAnswer(" not in text:
|
|
259
|
+
problems.append(
|
|
260
|
+
f"{agent_py} has no StructuredAnswer() (last) in middleware(): an answer that does "
|
|
261
|
+
"not fit would not go back to the model, and its run would end with "
|
|
262
|
+
"invalid_structured_response"
|
|
263
|
+
)
|
|
264
|
+
return problems
|
|
@@ -0,0 +1,319 @@
|
|
|
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
|
+
"""Subprocess helpers for agents CLI."""
|
|
16
|
+
|
|
17
|
+
import io
|
|
18
|
+
import os
|
|
19
|
+
import shlex
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
import click
|
|
26
|
+
|
|
27
|
+
from graph_agents_cli import _tools
|
|
28
|
+
|
|
29
|
+
# Set for an extension's command so any re-entry into `graph-agents-cli <cmd>` runs the
|
|
30
|
+
# built-in instead of recursing back into the override.
|
|
31
|
+
DISABLE_OVERRIDES_ENV = "GRAPH_AGENTS_CLI_DISABLE_OVERRIDES"
|
|
32
|
+
# Exported so an extension's scripts can find sibling resources (templates etc.).
|
|
33
|
+
EXTENSION_DIR_ENV = "GRAPH_AGENTS_CLI_EXTENSION_DIR"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def redact_cmd(args: list[str]) -> str:
|
|
37
|
+
"""Mask sensitive information in command arguments and return joined string.
|
|
38
|
+
|
|
39
|
+
Masks arguments like --github-pat and environment variables containing secrets.
|
|
40
|
+
"""
|
|
41
|
+
redacted_cmd_list = list(args)
|
|
42
|
+
for i, arg in enumerate(args):
|
|
43
|
+
if arg == "--github-pat" and i + 1 < len(args):
|
|
44
|
+
redacted_cmd_list[i + 1] = "[REDACTED]"
|
|
45
|
+
elif any(
|
|
46
|
+
secret in arg for secret in ["GITHUB_PAT", "GH_TOKEN", "GITHUB_TOKEN", "GITHUB_APP_KEY"]
|
|
47
|
+
):
|
|
48
|
+
redacted_cmd_list[i] = "[REDACTED]"
|
|
49
|
+
|
|
50
|
+
return shlex.join(redacted_cmd_list)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run(
|
|
54
|
+
args: list[str],
|
|
55
|
+
*,
|
|
56
|
+
cwd: str | Path | None = None,
|
|
57
|
+
env: dict | None = None,
|
|
58
|
+
capture: bool = False,
|
|
59
|
+
print_cmd: bool = True,
|
|
60
|
+
check: bool = True,
|
|
61
|
+
check_err_msg: str | None = None,
|
|
62
|
+
input_data: bytes | None = None,
|
|
63
|
+
timeout: int | None = None,
|
|
64
|
+
resolve_executable: bool = True,
|
|
65
|
+
) -> subprocess.CompletedProcess:
|
|
66
|
+
"""Run a subprocess, streaming output by default.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
args: Command and arguments.
|
|
70
|
+
cwd: Working directory for the subprocess.
|
|
71
|
+
env: Extra environment variables. Merged with os.environ if provided.
|
|
72
|
+
capture: If True, capture stdout/stderr instead of streaming.
|
|
73
|
+
Defaults to False.
|
|
74
|
+
print_cmd: If True, print the command before executing.
|
|
75
|
+
Defaults to True.
|
|
76
|
+
check: If True, raise ClickException on non-zero exit.
|
|
77
|
+
Defaults to True.
|
|
78
|
+
check_err_msg: Error message prefix for check failures.
|
|
79
|
+
input_data: Bytes to feed to stdin of the subprocess.
|
|
80
|
+
timeout: Timeout in seconds for the subprocess.
|
|
81
|
+
resolve_executable: If True, resolve the executable path using require_tool.
|
|
82
|
+
Defaults to True.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
CompletedProcess instance.
|
|
86
|
+
"""
|
|
87
|
+
cmd_str = redact_cmd(args)
|
|
88
|
+
|
|
89
|
+
if print_cmd:
|
|
90
|
+
click.secho(f" ▸ {cmd_str}", fg="cyan", dim=True)
|
|
91
|
+
|
|
92
|
+
run_env = None
|
|
93
|
+
if env is not None:
|
|
94
|
+
run_env = {**os.environ, **env}
|
|
95
|
+
|
|
96
|
+
# Capture output as UTF-8 text (replacing undecodable bytes) unless we're
|
|
97
|
+
# piping raw bytes to stdin, where child output must stay bytes. Without an
|
|
98
|
+
# explicit encoding, subprocess uses the locale codec + strict errors, which
|
|
99
|
+
# raises UnicodeDecodeError on non-UTF-8 locales (e.g. cp1252 on Windows).
|
|
100
|
+
text_mode = input_data is None
|
|
101
|
+
text_kwargs = {"encoding": "utf-8", "errors": "replace"} if text_mode else {}
|
|
102
|
+
|
|
103
|
+
if capture:
|
|
104
|
+
result = run_resolved(
|
|
105
|
+
args,
|
|
106
|
+
resolve_executable=resolve_executable,
|
|
107
|
+
capture_output=True,
|
|
108
|
+
text=text_mode,
|
|
109
|
+
cwd=str(cwd) if cwd else None,
|
|
110
|
+
input=input_data,
|
|
111
|
+
env=run_env,
|
|
112
|
+
timeout=timeout,
|
|
113
|
+
**text_kwargs,
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
# Under click.testing.CliRunner, sys.stdout is a StringIO-like object
|
|
117
|
+
# that doesn't have a fileno(). Subprocess on Windows requires a fileno.
|
|
118
|
+
# We check carefully to avoid issues with mocks or unusual environments.
|
|
119
|
+
use_fallback = True
|
|
120
|
+
if hasattr(sys.stdout, "fileno") and callable(sys.stdout.fileno):
|
|
121
|
+
try:
|
|
122
|
+
sys.stdout.fileno()
|
|
123
|
+
use_fallback = False
|
|
124
|
+
except (io.UnsupportedOperation, AttributeError, OSError):
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
if not use_fallback:
|
|
128
|
+
result = run_resolved(
|
|
129
|
+
args,
|
|
130
|
+
resolve_executable=resolve_executable,
|
|
131
|
+
stdout=sys.stdout,
|
|
132
|
+
stderr=sys.stderr,
|
|
133
|
+
cwd=str(cwd) if cwd else None,
|
|
134
|
+
input=input_data,
|
|
135
|
+
env=run_env,
|
|
136
|
+
timeout=timeout,
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
# Fallback to capture and manual emit for environments without a
|
|
140
|
+
# real stdout fileno (e.g. click.testing.CliRunner).
|
|
141
|
+
result = run_resolved(
|
|
142
|
+
args,
|
|
143
|
+
resolve_executable=resolve_executable,
|
|
144
|
+
capture_output=True,
|
|
145
|
+
text=text_mode,
|
|
146
|
+
cwd=str(cwd) if cwd else None,
|
|
147
|
+
input=input_data,
|
|
148
|
+
env=run_env,
|
|
149
|
+
timeout=timeout,
|
|
150
|
+
**text_kwargs,
|
|
151
|
+
)
|
|
152
|
+
# stdout/stderr are bytes when input_data is set (text=False).
|
|
153
|
+
for stream, content in (
|
|
154
|
+
(sys.stdout, result.stdout),
|
|
155
|
+
(sys.stderr, result.stderr),
|
|
156
|
+
):
|
|
157
|
+
if not content:
|
|
158
|
+
continue
|
|
159
|
+
if isinstance(content, bytes):
|
|
160
|
+
content = content.decode(errors="replace")
|
|
161
|
+
stream.write(content)
|
|
162
|
+
|
|
163
|
+
if check and result.returncode != 0:
|
|
164
|
+
error_msg = check_err_msg or f"Command failed: {cmd_str}"
|
|
165
|
+
# When output was captured it never reached the console, so fold it into
|
|
166
|
+
# the error — otherwise the failure reason is lost. Streamed runs already
|
|
167
|
+
# printed it. stdout/stderr are bytes when input_data is set (text=False).
|
|
168
|
+
detail = ""
|
|
169
|
+
if capture:
|
|
170
|
+
captured = "\n".join(
|
|
171
|
+
part.strip()
|
|
172
|
+
for part in (result.stdout, result.stderr)
|
|
173
|
+
if isinstance(part, str) and part.strip()
|
|
174
|
+
)
|
|
175
|
+
if captured:
|
|
176
|
+
detail = f"\n{captured}"
|
|
177
|
+
raise click.ClickException(f"{error_msg} (exit code {result.returncode}){detail}")
|
|
178
|
+
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _looks_like_path(token: str) -> bool:
|
|
183
|
+
"""A `run:` token the author wrote as a path, e.g. `scripts/run.py`."""
|
|
184
|
+
return "/" in token or os.sep in token
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def run_extension_command(
|
|
188
|
+
run_vector: Sequence[str],
|
|
189
|
+
extra_args: list[str],
|
|
190
|
+
*,
|
|
191
|
+
cwd: Path,
|
|
192
|
+
extension_root: Path,
|
|
193
|
+
) -> int:
|
|
194
|
+
"""Run an extension's declared `run:` vector with the user's argv appended.
|
|
195
|
+
|
|
196
|
+
Differs from `run` in ways an override needs: the vector is extension-supplied
|
|
197
|
+
and passed through verbatim (no `require_tool` resolution), the command is
|
|
198
|
+
not echoed, and the child's exit code is returned rather than raised, so the
|
|
199
|
+
override exits like the command it replaced.
|
|
200
|
+
|
|
201
|
+
Tokens that exist as paths under `extension_root` are made absolute, so a
|
|
202
|
+
vendored script is found regardless of cwd.
|
|
203
|
+
"""
|
|
204
|
+
# Only tokens written as paths. Resolving every token would rewrite an
|
|
205
|
+
# ordinary argument that happens to name a file in the extension, e.g.
|
|
206
|
+
# `run: ["make", "build"]` in an extension that also ships `build/`.
|
|
207
|
+
resolved_vector = [
|
|
208
|
+
str((extension_root / tok).resolve())
|
|
209
|
+
if _looks_like_path(tok) and (extension_root / tok).exists()
|
|
210
|
+
else tok
|
|
211
|
+
for tok in run_vector
|
|
212
|
+
]
|
|
213
|
+
result = run(
|
|
214
|
+
resolved_vector + list(extra_args),
|
|
215
|
+
cwd=cwd,
|
|
216
|
+
env={DISABLE_OVERRIDES_ENV: "1", EXTENSION_DIR_ENV: str(extension_root)},
|
|
217
|
+
print_cmd=False,
|
|
218
|
+
check=False,
|
|
219
|
+
resolve_executable=False,
|
|
220
|
+
)
|
|
221
|
+
return result.returncode
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def run_resolved(
|
|
225
|
+
args: list[str], *, resolve_executable: bool = True, **kwargs
|
|
226
|
+
) -> subprocess.CompletedProcess:
|
|
227
|
+
"""Wrapper around subprocess.run with optional executable resolution.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
args: Command and arguments as a list of strings.
|
|
231
|
+
resolve_executable: If True, resolve the executable path using require_tool.
|
|
232
|
+
Defaults to True.
|
|
233
|
+
**kwargs: Additional keyword arguments passed to subprocess.run.
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
ToolNotFoundError: If resolve_executable is True and the tool cannot be found.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
CompletedProcess instance.
|
|
240
|
+
"""
|
|
241
|
+
if isinstance(args, str):
|
|
242
|
+
raise ValueError("args must be a list of strings, not a single string.")
|
|
243
|
+
|
|
244
|
+
if resolve_executable and args:
|
|
245
|
+
executable = args[0]
|
|
246
|
+
# Create a shallow copy to avoid modifying the original list passed by reference
|
|
247
|
+
args = args.copy()
|
|
248
|
+
args[0] = _tools.require_tool(executable)
|
|
249
|
+
|
|
250
|
+
return subprocess.run(args, **kwargs)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def popen_resolved(
|
|
254
|
+
args: list[str], *, resolve_executable: bool = True, **kwargs
|
|
255
|
+
) -> subprocess.Popen:
|
|
256
|
+
"""Wrapper around subprocess.Popen with optional executable resolution.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
args: Command and arguments as a list of strings.
|
|
260
|
+
resolve_executable: If True, resolve the executable path using require_tool.
|
|
261
|
+
Defaults to True.
|
|
262
|
+
**kwargs: Additional keyword arguments passed to subprocess.Popen.
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
ToolNotFoundError: If resolve_executable is True and the tool cannot be found.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Popen instance.
|
|
269
|
+
"""
|
|
270
|
+
if isinstance(args, str):
|
|
271
|
+
raise ValueError("args must be a list of strings, not a single string.")
|
|
272
|
+
|
|
273
|
+
if resolve_executable and args:
|
|
274
|
+
executable = args[0]
|
|
275
|
+
# Create a shallow copy to avoid modifying the original list passed by reference
|
|
276
|
+
args = args.copy()
|
|
277
|
+
args[0] = _tools.require_tool(executable)
|
|
278
|
+
|
|
279
|
+
return subprocess.Popen(args, **kwargs)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def popen_resolved_detached(
|
|
283
|
+
args: list[str], *, resolve_executable: bool = True, **kwargs
|
|
284
|
+
) -> subprocess.Popen:
|
|
285
|
+
"""Wrapper around subprocess.Popen for launching detached background processes.
|
|
286
|
+
|
|
287
|
+
Handles cross-platform differences for process detaching:
|
|
288
|
+
- On POSIX, sets start_new_session=True.
|
|
289
|
+
- On Windows, sets creationflags to DETACHED_PROCESS and CREATE_NEW_PROCESS_GROUP.
|
|
290
|
+
- Ensures stdin is redirected to subprocess.DEVNULL.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
args: Command and arguments as a list of strings.
|
|
294
|
+
resolve_executable: If True, resolve the executable path using require_tool.
|
|
295
|
+
Defaults to True.
|
|
296
|
+
**kwargs: Additional keyword arguments passed to subprocess.Popen.
|
|
297
|
+
|
|
298
|
+
Raises:
|
|
299
|
+
ToolNotFoundError: If resolve_executable is True and the tool cannot be found.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Popen instance.
|
|
303
|
+
"""
|
|
304
|
+
stdin = kwargs.get("stdin")
|
|
305
|
+
if stdin is not None and stdin is not subprocess.DEVNULL:
|
|
306
|
+
raise ValueError("popen_resolved_detached only supports stdin=DEVNULL.")
|
|
307
|
+
kwargs["stdin"] = subprocess.DEVNULL
|
|
308
|
+
|
|
309
|
+
if os.name == "nt":
|
|
310
|
+
# Windows-specific process creation flags for detaching
|
|
311
|
+
create_new_process_group = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
|
312
|
+
create_no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
|
313
|
+
flags = create_new_process_group | create_no_window
|
|
314
|
+
kwargs["creationflags"] = kwargs.get("creationflags", 0) | flags
|
|
315
|
+
else:
|
|
316
|
+
# POSIX way of detaching
|
|
317
|
+
kwargs["start_new_session"] = True
|
|
318
|
+
|
|
319
|
+
return popen_resolved(args, resolve_executable=resolve_executable, **kwargs)
|