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