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