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,894 @@
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
+ """3-way file comparison and dependency merging for upgrade and enhance."""
17
+
18
+ import fnmatch
19
+ import hashlib
20
+ import logging
21
+ import pathlib
22
+ import re
23
+ import tomllib
24
+ from collections.abc import Callable
25
+ from dataclasses import dataclass
26
+ from typing import Any, Literal
27
+
28
+ import yaml
29
+
30
+ MANIFEST_FILENAME = "graph-agents-cli-manifest.yaml"
31
+
32
+ # Preservation rules: what upgrade/enhance never overwrite. Patterns use the
33
+ # {agent_directory} placeholder, replaced at runtime. Everything not listed is
34
+ # "scaffolding" and gets the 3-way compare.
35
+ FILE_CATEGORIES: dict[str, list[str]] = {
36
+ "agent_code": [ # Never modified
37
+ "{agent_directory}/agent.py",
38
+ "{agent_directory}/tools/**",
39
+ "{agent_directory}/policies/**",
40
+ # The shape of the agent's final answer (structured answers), when it has one.
41
+ "{agent_directory}/response_schema.json",
42
+ # Reserved for directories the developer may create.
43
+ "{agent_directory}/prompts/**",
44
+ "{agent_directory}/graph/**",
45
+ ],
46
+ "config_files": [ # Never overwritten
47
+ ".env",
48
+ ".env.*",
49
+ "api-policy.yaml",
50
+ "deployment/helm/*/values-*.yaml",
51
+ "deployment/argocd/**",
52
+ "tests/eval/datasets/**",
53
+ "tests/eval/eval_config.yaml",
54
+ ],
55
+ "dependencies": [ # Semantic merge
56
+ "pyproject.toml",
57
+ MANIFEST_FILENAME,
58
+ ],
59
+ }
60
+
61
+
62
+ # Config files a runtime, model provider, target or CD change re-renders. Under
63
+ # ``scaffold enhance`` (``merge_config=True``) they are merged three-way instead
64
+ # of skipped (config files) or kept whole on the first edit (the chart's
65
+ # values.yaml, .github/agent.env): an untouched copy takes the template's new
66
+ # version, an edited one gets the template's change applied around the
67
+ # developer's edits (line by line, else key by key), and a key the developer
68
+ # changed too is left alone and reported. ``.env`` (secrets), ``api-policy.yaml``
69
+ # and the eval datasets never are: an enhance does not change them.
70
+ STRUCTURAL_CONFIG_FILES: list[str] = [
71
+ ".env.example",
72
+ ".github/agent.env",
73
+ "deployment/helm/*/values.yaml",
74
+ "deployment/helm/*/values-*.yaml",
75
+ "deployment/argocd/**",
76
+ ]
77
+
78
+ # Files that define the container image. Under enhance, a template change to
79
+ # one of them that could not be applied leaves the image on the old settings
80
+ # (the fastapi image under the langgraph-server runtime, say), so the project
81
+ # does not build what the manifest says until the developer merges it by hand.
82
+ IMAGE_DEFINITION_FILES: list[str] = ["Dockerfile"]
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class Followup:
87
+ """Something the developer still has to do by hand after upgrade/enhance.
88
+
89
+ ``required``: until it is done the project does not build or deploy with
90
+ the settings the manifest records (enhance then exits non-zero).
91
+ """
92
+
93
+ text: str
94
+ required: bool = False
95
+
96
+
97
+ # Preserve type literals for type-safe reason matching
98
+ PreserveType = Literal["gacli_unchanged", "already_current", "unchanged_both", None]
99
+
100
+
101
+ @dataclass
102
+ class FileCompareResult:
103
+ """Result of comparing a file across three versions."""
104
+
105
+ path: str
106
+ category: str
107
+ action: Literal["auto_update", "preserve", "skip", "conflict", "new", "removed", "merge"]
108
+ reason: str
109
+ # For preserve actions, indicates why preserved
110
+ preserve_type: PreserveType = None
111
+ # For conflicts, store the content hashes
112
+ current_hash: str | None = None
113
+ old_template_hash: str | None = None
114
+ new_template_hash: str | None = None
115
+ # Something the developer still has to do for this file (printed after the run).
116
+ followup: str | Followup | None = None
117
+
118
+
119
+ class DependencyReadError(Exception):
120
+ """A dependency manifest could not be read: it's missing or unparseable."""
121
+
122
+
123
+ @dataclass
124
+ class DependencyResolution:
125
+ """What the merge decided to do with one dependency.
126
+
127
+ Statuses:
128
+ - "added": the template introduced this dependency.
129
+ - "updated": a template-managed dependency whose spec the template changed.
130
+ - "removed": a template-managed dependency the new template dropped.
131
+ - "unchanged": a template-managed dependency the template left as-is.
132
+ - "kept": a dependency *the user* added, preserved untouched.
133
+ """
134
+
135
+ name: str
136
+ status: Literal["updated", "added", "removed", "kept", "unchanged"]
137
+ old_version: str | None = None
138
+ new_version: str | None = None
139
+
140
+ def full_name(self) -> str:
141
+ """The full requirement to write: ``name[extra]version``."""
142
+ return f"{self.name}{self.new_version or ''}"
143
+
144
+
145
+ def _expand_patterns(patterns: list[str], agent_directory: str) -> list[str]:
146
+ """Expand {agent_directory} placeholder in patterns."""
147
+ return [p.replace("{agent_directory}", agent_directory) for p in patterns]
148
+
149
+
150
+ def _matches_any_pattern(path: str, patterns: list[str]) -> bool:
151
+ """Check if path matches any glob pattern, including ** recursive patterns."""
152
+ path = path.replace("\\", "/")
153
+
154
+ for pattern in patterns:
155
+ pattern = pattern.replace("\\", "/")
156
+
157
+ if fnmatch.fnmatch(path, pattern):
158
+ return True
159
+
160
+ if "**" in pattern:
161
+ regex = re.escape(pattern)
162
+ regex = regex.replace(r"\*\*/", "(?:.*/)?") # **/ = zero or more dirs
163
+ regex = regex.replace(r"\*\*", ".*")
164
+ regex = regex.replace(r"\*", "[^/]*")
165
+ if re.match(f"^{regex}$", path):
166
+ return True
167
+
168
+ return False
169
+
170
+
171
+ def is_structural_config(path: str) -> bool:
172
+ """True for a config file the enhanced settings re-render (``STRUCTURAL_CONFIG_FILES``)."""
173
+ return _matches_any_pattern(path, STRUCTURAL_CONFIG_FILES)
174
+
175
+
176
+ def is_image_definition(path: str) -> bool:
177
+ """True for a file that defines the container image (``IMAGE_DEFINITION_FILES``)."""
178
+ return _matches_any_pattern(path, IMAGE_DEFINITION_FILES)
179
+
180
+
181
+ def categorize_file(path: str, agent_directory: str = "app") -> str:
182
+ """Return category: agent_code, config_files, dependencies, or scaffolding."""
183
+ for category, patterns in FILE_CATEGORIES.items():
184
+ expanded = _expand_patterns(patterns, agent_directory)
185
+ if _matches_any_pattern(path, expanded):
186
+ return category
187
+ return "scaffolding"
188
+
189
+
190
+ def _file_hash(file_path: pathlib.Path) -> str | None:
191
+ """Calculate SHA256 hash of a file's contents."""
192
+ if not file_path.exists():
193
+ return None
194
+ try:
195
+ content = file_path.read_bytes()
196
+ return hashlib.sha256(content).hexdigest()
197
+ except Exception as e:
198
+ logging.warning(f"Could not hash file {file_path}: {e}")
199
+ return None
200
+
201
+
202
+ def three_way_compare(
203
+ relative_path: str,
204
+ project_dir: pathlib.Path,
205
+ old_template_dir: pathlib.Path,
206
+ new_template_dir: pathlib.Path,
207
+ agent_directory: str = "app",
208
+ *,
209
+ merge_config: bool = False,
210
+ ) -> FileCompareResult:
211
+ """Compare file across current, old template, and new template.
212
+
213
+ Returns action based on:
214
+ - current == old -> auto-update (user didn't modify)
215
+ - old == new -> preserve (the CLI didn't change)
216
+ - all differ -> conflict
217
+
218
+ ``merge_config`` (enhance) gives the ``STRUCTURAL_CONFIG_FILES`` a three-way
219
+ merge instead of the config-file skip.
220
+ """
221
+ category = categorize_file(relative_path, agent_directory)
222
+
223
+ current_file = project_dir / relative_path
224
+ old_template_file = old_template_dir / relative_path
225
+ new_template_file = new_template_dir / relative_path
226
+
227
+ if category in ("agent_code", "config_files") and not current_file.exists():
228
+ # Never modified / never overwritten: a file the project does not have
229
+ # yet is added only when the old snapshot did not ship it either (e.g.
230
+ # the argocd Applications that `enhance --cd argocd` introduces). When
231
+ # the old template shipped it and the project no longer has it, the
232
+ # developer removed it deliberately: it is never re-added.
233
+ new_hash = _file_hash(new_template_file)
234
+ if new_hash is not None:
235
+ if old_template_file.exists():
236
+ return FileCompareResult(
237
+ path=relative_path,
238
+ category=category,
239
+ action="skip",
240
+ reason="Removed by you (never re-added)",
241
+ new_template_hash=new_hash,
242
+ )
243
+ return FileCompareResult(
244
+ path=relative_path,
245
+ category=category,
246
+ action="new",
247
+ reason="New file in the template (added, then never touched again)",
248
+ new_template_hash=new_hash,
249
+ )
250
+
251
+ if category == "agent_code":
252
+ return FileCompareResult(
253
+ path=relative_path,
254
+ category=category,
255
+ action="skip",
256
+ reason="Agent code (never modified by upgrade)",
257
+ )
258
+
259
+ if (
260
+ merge_config
261
+ and category in ("config_files", "scaffolding")
262
+ and is_structural_config(relative_path)
263
+ ):
264
+ return _compare_structural_config(
265
+ relative_path, current_file, old_template_file, new_template_file, category
266
+ )
267
+
268
+ if category == "config_files":
269
+ return FileCompareResult(
270
+ path=relative_path,
271
+ category=category,
272
+ action="skip",
273
+ reason="Config file (user's environment settings)",
274
+ )
275
+
276
+ if category == "dependencies":
277
+ return FileCompareResult(
278
+ path=relative_path,
279
+ category=category,
280
+ action="preserve",
281
+ reason="Dependencies (requires merge handling)",
282
+ )
283
+
284
+ current_hash = _file_hash(current_file)
285
+ old_hash = _file_hash(old_template_file)
286
+ new_hash = _file_hash(new_template_file)
287
+
288
+ # New file in the template (not in project, regardless of old template)
289
+ if current_hash is None and new_hash is not None:
290
+ return FileCompareResult(
291
+ path=relative_path,
292
+ category=category,
293
+ action="new",
294
+ reason="New file in the template",
295
+ new_template_hash=new_hash,
296
+ )
297
+
298
+ # File removed in new template
299
+ if current_hash is not None and old_hash is not None and new_hash is None:
300
+ if current_hash == old_hash:
301
+ return FileCompareResult(
302
+ path=relative_path,
303
+ category=category,
304
+ action="removed",
305
+ reason="File removed from the template (you didn't modify it)",
306
+ current_hash=current_hash,
307
+ old_template_hash=old_hash,
308
+ )
309
+ return FileCompareResult(
310
+ path=relative_path,
311
+ category=category,
312
+ action="conflict",
313
+ reason="File removed from the template but you modified it",
314
+ current_hash=current_hash,
315
+ old_template_hash=old_hash,
316
+ )
317
+
318
+ # File only in current project (user-added, not part of the template)
319
+ if current_hash is not None and old_hash is None and new_hash is None:
320
+ return FileCompareResult(
321
+ path=relative_path,
322
+ category=category,
323
+ action="skip",
324
+ reason="User-added file (not part of the template)",
325
+ )
326
+
327
+ # File doesn't exist anywhere relevant
328
+ if current_hash is None and new_hash is None:
329
+ return FileCompareResult(
330
+ path=relative_path,
331
+ category=category,
332
+ action="skip",
333
+ reason="File not present",
334
+ )
335
+
336
+ # User didn't modify (current == old)
337
+ if current_hash == old_hash and new_hash is not None:
338
+ if old_hash == new_hash:
339
+ return FileCompareResult(
340
+ path=relative_path,
341
+ category=category,
342
+ action="preserve",
343
+ reason="Unchanged in both project and template",
344
+ preserve_type="unchanged_both",
345
+ current_hash=current_hash,
346
+ old_template_hash=old_hash,
347
+ new_template_hash=new_hash,
348
+ )
349
+ return FileCompareResult(
350
+ path=relative_path,
351
+ category=category,
352
+ action="auto_update",
353
+ reason="You didn't modify this file",
354
+ current_hash=current_hash,
355
+ old_template_hash=old_hash,
356
+ new_template_hash=new_hash,
357
+ )
358
+
359
+ # The template didn't change (old == new)
360
+ if old_hash == new_hash and current_hash is not None:
361
+ return FileCompareResult(
362
+ path=relative_path,
363
+ category=category,
364
+ action="preserve",
365
+ reason="The template didn't change this file",
366
+ preserve_type="gacli_unchanged",
367
+ current_hash=current_hash,
368
+ old_template_hash=old_hash,
369
+ new_template_hash=new_hash,
370
+ )
371
+
372
+ # Already up to date (current == new)
373
+ if current_hash == new_hash:
374
+ return FileCompareResult(
375
+ path=relative_path,
376
+ category=category,
377
+ action="preserve",
378
+ reason="Already up to date",
379
+ preserve_type="already_current",
380
+ current_hash=current_hash,
381
+ old_template_hash=old_hash,
382
+ new_template_hash=new_hash,
383
+ )
384
+
385
+ # All three differ -> conflict
386
+ return FileCompareResult(
387
+ path=relative_path,
388
+ category=category,
389
+ action="conflict",
390
+ reason="Both you and the template modified this file",
391
+ current_hash=current_hash,
392
+ old_template_hash=old_hash,
393
+ new_template_hash=new_hash,
394
+ )
395
+
396
+
397
+ def _read_text(path: pathlib.Path) -> str | None:
398
+ try:
399
+ return path.read_text(encoding="utf-8")
400
+ except (OSError, UnicodeDecodeError):
401
+ return None
402
+
403
+
404
+ def _compare_structural_config(
405
+ relative_path: str,
406
+ current_file: pathlib.Path,
407
+ old_template_file: pathlib.Path,
408
+ new_template_file: pathlib.Path,
409
+ category: str,
410
+ ) -> FileCompareResult:
411
+ """Three-way handling of a config file the enhanced settings re-render (see STRUCTURAL_CONFIG_FILES)."""
412
+ from .merge3 import template_diff
413
+
414
+ current_hash = _file_hash(current_file)
415
+ old_hash = _file_hash(old_template_file)
416
+ new_hash = _file_hash(new_template_file)
417
+ hashes = {
418
+ "current_hash": current_hash,
419
+ "old_template_hash": old_hash,
420
+ "new_template_hash": new_hash,
421
+ }
422
+ if current_hash is None and new_hash is None:
423
+ return FileCompareResult(
424
+ path=relative_path,
425
+ category=category,
426
+ action="skip",
427
+ reason="File not present",
428
+ **hashes,
429
+ )
430
+ if current_hash is None and category == "scaffolding":
431
+ # Like any scaffolding file the project lacks (config files are handled
432
+ # before this: one the developer removed is never re-added).
433
+ return FileCompareResult(
434
+ path=relative_path,
435
+ category=category,
436
+ action="new",
437
+ reason="New file in the template",
438
+ **hashes,
439
+ )
440
+ if old_hash == new_hash:
441
+ return FileCompareResult(
442
+ path=relative_path,
443
+ category=category,
444
+ action="skip",
445
+ reason="Config file (not changed by the new settings)",
446
+ **hashes,
447
+ )
448
+ if new_hash is None and category == "scaffolding" and current_hash is not None:
449
+ # Gone with the rest of what the new settings no longer render (the whole
450
+ # chart when the target leaves kubernetes), as any scaffolding file.
451
+ if current_hash == old_hash:
452
+ return FileCompareResult(
453
+ path=relative_path,
454
+ category=category,
455
+ action="removed",
456
+ reason="File removed from the template (you didn't modify it)",
457
+ **hashes,
458
+ )
459
+ return FileCompareResult(
460
+ path=relative_path,
461
+ category=category,
462
+ action="conflict",
463
+ reason="File removed from the template but you modified it",
464
+ **hashes,
465
+ )
466
+ if new_hash is None:
467
+ return FileCompareResult(
468
+ path=relative_path,
469
+ category=category,
470
+ action="skip",
471
+ reason="Config file the new settings no longer render (kept)",
472
+ followup=(
473
+ f"{relative_path}: no longer part of the template for the new settings; it "
474
+ "was kept, delete it if nothing uses it any more"
475
+ ),
476
+ **hashes,
477
+ )
478
+ if current_hash == new_hash:
479
+ return FileCompareResult(
480
+ path=relative_path,
481
+ category=category,
482
+ action="preserve",
483
+ reason="Already up to date",
484
+ preserve_type="already_current",
485
+ **hashes,
486
+ )
487
+ if current_hash == old_hash:
488
+ return FileCompareResult(
489
+ path=relative_path,
490
+ category=category,
491
+ action="auto_update",
492
+ reason="Config file you did not modify: updated for the new settings",
493
+ **hashes,
494
+ )
495
+ old_text = _read_text(old_template_file) if old_hash is not None else ""
496
+ current_text = _read_text(current_file)
497
+ new_text = _read_text(new_template_file)
498
+ merged, conflicts = (
499
+ (None, [])
500
+ if old_text is None or current_text is None or new_text is None
501
+ else _merge_config_texts(old_text, current_text, new_text, relative_path)
502
+ )
503
+ if merged is not None and merged != current_text:
504
+ return FileCompareResult(
505
+ path=relative_path,
506
+ category=category,
507
+ action="merge",
508
+ reason="Config file you modified: the template's change is merged around your edits",
509
+ followup=_kept_keys_followup(relative_path, conflicts, partly=True),
510
+ **hashes,
511
+ )
512
+ if merged is not None and conflicts:
513
+ return FileCompareResult(
514
+ path=relative_path,
515
+ category=category,
516
+ action="skip",
517
+ reason="Config file you modified where the template changed the same keys (kept)",
518
+ followup=_kept_keys_followup(relative_path, conflicts, partly=False),
519
+ **hashes,
520
+ )
521
+ if merged is not None:
522
+ # Every key already reads as the new settings want; only comments differ.
523
+ return FileCompareResult(
524
+ path=relative_path,
525
+ category=category,
526
+ action="preserve",
527
+ reason="Already up to date (your edits carry the new settings)",
528
+ preserve_type="already_current",
529
+ **hashes,
530
+ )
531
+ diff = template_diff(old_text or "", new_text or "", relative_path)
532
+ return FileCompareResult(
533
+ path=relative_path,
534
+ category=category,
535
+ action="skip",
536
+ reason="Config file you modified where the template changed the same lines (kept)",
537
+ followup=(
538
+ f"{relative_path}: your edits overlap the template's change for the new settings, "
539
+ "so the file was left as it is. Apply this change by hand:\n" + diff
540
+ ),
541
+ **hashes,
542
+ )
543
+
544
+
545
+ def _kept_keys_followup(relative_path: str, conflicts: list[Any], *, partly: bool) -> str | None:
546
+ """The keys of ``relative_path`` the developer changed that the new settings change too."""
547
+ if not conflicts:
548
+ return None
549
+ head = (
550
+ "the new settings' change was applied except to keys you changed too"
551
+ if partly
552
+ else "left as it is, because you changed the keys the new settings change"
553
+ )
554
+ return (
555
+ f"{relative_path}: {head}. Your values were kept; change them by hand if they no "
556
+ "longer fit:\n" + "".join(f" {conflict.describe()}\n" for conflict in conflicts)
557
+ )
558
+
559
+
560
+ def _merge_config_texts(
561
+ old_text: str, current_text: str, new_text: str, relative_path: str
562
+ ) -> tuple[str | None, list[Any]]:
563
+ """The developer's config with the template's change applied, and the keys left alone.
564
+
565
+ The line merge first (it also carries the template's comment changes); when
566
+ the edits are too close for it, the key-by-key merge, which applies every
567
+ changed key the developer did not change too and returns the others.
568
+ ``(None, [])`` when neither can merge the file safely.
569
+ """
570
+ from .keymerge import merge_keys
571
+ from .merge3 import merge3_checked
572
+
573
+ merged = merge3_checked(old_text, current_text, new_text, relative_path)
574
+ if merged is not None:
575
+ return merged, []
576
+ result = merge_keys(old_text, current_text, new_text, relative_path)
577
+ if result is None:
578
+ return None, []
579
+ return result.text, list(result.conflicts)
580
+
581
+
582
+ def merged_config_text(
583
+ project_dir: pathlib.Path,
584
+ old_template_dir: pathlib.Path,
585
+ new_template_dir: pathlib.Path,
586
+ relative_path: str,
587
+ ) -> str | None:
588
+ """The three-way merge of a ``merge`` result, or None when it no longer merges cleanly."""
589
+ old_file = old_template_dir / relative_path
590
+ old_text = _read_text(old_file) if old_file.exists() else ""
591
+ current_text = _read_text(project_dir / relative_path)
592
+ new_text = _read_text(new_template_dir / relative_path)
593
+ if old_text is None or current_text is None or new_text is None:
594
+ return None
595
+ return _merge_config_texts(old_text, current_text, new_text, relative_path)[0]
596
+
597
+
598
+ def collect_all_files(
599
+ project_dir: pathlib.Path,
600
+ old_template_dir: pathlib.Path,
601
+ new_template_dir: pathlib.Path,
602
+ exclude_patterns: list[str] | None = None,
603
+ ) -> set[str]:
604
+ """Collect all unique relative file paths from all three directories."""
605
+ if exclude_patterns is None:
606
+ exclude_patterns = [
607
+ ".git/**",
608
+ ".venv/**",
609
+ "venv/**",
610
+ "__pycache__/**",
611
+ "*.pyc",
612
+ ".DS_Store",
613
+ "*.egg-info/**",
614
+ "uv.lock",
615
+ ".uv/**",
616
+ ".graph-agents-cli/**",
617
+ ]
618
+
619
+ all_files: set[str] = set()
620
+
621
+ for base_dir in [project_dir, old_template_dir, new_template_dir]:
622
+ if not base_dir.exists():
623
+ continue
624
+ for file_path in base_dir.rglob("*"):
625
+ if file_path.is_file():
626
+ relative = str(file_path.relative_to(base_dir))
627
+ if not _matches_any_pattern(relative, exclude_patterns):
628
+ all_files.add(relative)
629
+
630
+ return all_files
631
+
632
+
633
+ def _parse_dependency(dep_str: str) -> tuple[str, str, str]:
634
+ """Parse a dependency string into (base_name, extras, version_spec).
635
+
636
+ Examples:
637
+ "langgraph>=1.0" -> ("langgraph", "", ">=1.0")
638
+ "a2a-sdk[http-server]>=1.0" -> ("a2a-sdk", "[http-server]", ">=1.0")
639
+ "pytest" -> ("pytest", "", "")
640
+ """
641
+ match = re.match(r"^([a-zA-Z0-9_-]+)(\[[^\]]+\])?(.*)", dep_str.strip())
642
+ if match:
643
+ base_name = match.group(1).lower()
644
+ extras = match.group(2) or ""
645
+ version = match.group(3).strip()
646
+ return base_name, extras, version
647
+ return dep_str.lower(), "", ""
648
+
649
+
650
+ def _load_dependencies_from_pyproject(pyproject_path: pathlib.Path) -> dict[str, str]:
651
+ """Load dependencies as {base_name: version_spec} dict.
652
+
653
+ Raises DependencyReadError if pyproject.toml is missing or unparseable; a
654
+ present file with no dependencies yields {}.
655
+ """
656
+ if not pyproject_path.exists():
657
+ raise DependencyReadError(f"Dependency manifest not found: {pyproject_path}")
658
+
659
+ try:
660
+ with open(pyproject_path, "rb") as f:
661
+ data = tomllib.load(f)
662
+
663
+ deps = data.get("project", {}).get("dependencies", [])
664
+ result: dict[str, str] = {}
665
+ for dep in deps:
666
+ name, extras, version = _parse_dependency(dep)
667
+ result[name] = f"{extras}{version}"
668
+ return result
669
+ except Exception as e:
670
+ raise DependencyReadError(f"Could not read dependencies from {pyproject_path}: {e}") from e
671
+
672
+
673
+ def _resolve_dependencies(
674
+ current: dict[str, str],
675
+ old: dict[str, str],
676
+ new: dict[str, str],
677
+ ) -> list[DependencyResolution]:
678
+ """Merge deps from three sources: new template + user-added (current - old)."""
679
+ resolutions: list[DependencyResolution] = []
680
+ user_added = set(current) - set(old)
681
+ gacli_managed = set(old)
682
+
683
+ for name, new_spec in new.items():
684
+ if name in old:
685
+ old_spec = old[name]
686
+ resolutions.append(
687
+ DependencyResolution(
688
+ name=name,
689
+ status="updated" if old_spec != new_spec else "unchanged",
690
+ old_version=old_spec,
691
+ new_version=new_spec,
692
+ )
693
+ )
694
+ elif name in user_added:
695
+ user_spec = current[name]
696
+ logging.warning(
697
+ f"Dependency '{name}' is now added by the template ({new_spec}) "
698
+ f"but your project already pins it ({user_spec}). Keeping your "
699
+ f"version; if the upgrade misbehaves, reconcile it with the "
700
+ f"template's expected {new_spec}."
701
+ )
702
+ else:
703
+ resolutions.append(
704
+ DependencyResolution(name=name, status="added", new_version=new_spec)
705
+ )
706
+
707
+ for name in user_added:
708
+ user_spec = current[name]
709
+ resolutions.append(
710
+ DependencyResolution(
711
+ name=name, status="kept", old_version=user_spec, new_version=user_spec
712
+ )
713
+ )
714
+
715
+ for name in gacli_managed:
716
+ if name not in new and name not in user_added:
717
+ resolutions.append(
718
+ DependencyResolution(name=name, status="removed", old_version=old[name])
719
+ )
720
+
721
+ return resolutions
722
+
723
+
724
+ def merge_python_dependencies(
725
+ current_project: pathlib.Path,
726
+ old_template_project: pathlib.Path,
727
+ new_template_project: pathlib.Path,
728
+ ) -> list[DependencyResolution]:
729
+ """Merge Python deps: new_template + user_added, where user_added = current - old."""
730
+ return _resolve_dependencies(
731
+ _load_dependencies_from_pyproject(current_project / "pyproject.toml"),
732
+ _load_dependencies_from_pyproject(old_template_project / "pyproject.toml"),
733
+ _load_dependencies_from_pyproject(new_template_project / "pyproject.toml"),
734
+ )
735
+
736
+
737
+ def write_python_dependencies(
738
+ project_dir: pathlib.Path,
739
+ resolutions: list[DependencyResolution],
740
+ ) -> bool:
741
+ """Write merged dependencies to project_dir/pyproject.toml using the uv CLI.
742
+
743
+ Uses ``uv add --frozen`` and ``uv remove --frozen`` so the lockfile and
744
+ virtualenv are left untouched; only pyproject.toml is modified.
745
+ """
746
+ pyproject_path = project_dir / "pyproject.toml"
747
+ if not pyproject_path.exists():
748
+ return False
749
+
750
+ try:
751
+ from graph_agents_cli._runner import run_resolved
752
+ from graph_agents_cli._tools import ToolNotFoundError
753
+
754
+ ordered: list[DependencyResolution] = sorted(resolutions, key=lambda r: r.name)
755
+ to_remove = [r.name for r in ordered if r.status == "removed"]
756
+ to_add = [r.full_name() for r in ordered if r.status != "removed"]
757
+
758
+ if to_remove:
759
+ result = run_resolved(
760
+ ["uv", "remove", "--frozen", *to_remove],
761
+ cwd=project_dir,
762
+ capture_output=True,
763
+ text=True,
764
+ )
765
+ if result.returncode != 0:
766
+ logging.warning(f"uv remove failed: {result.stderr}")
767
+
768
+ if to_add:
769
+ result = run_resolved(
770
+ ["uv", "add", "--frozen", *to_add],
771
+ cwd=project_dir,
772
+ capture_output=True,
773
+ text=True,
774
+ )
775
+ if result.returncode != 0:
776
+ logging.warning(f"uv add failed: {result.stderr}")
777
+ return False
778
+
779
+ return True
780
+ except ToolNotFoundError:
781
+ logging.warning("uv not found; cannot write merged dependencies")
782
+ return False
783
+ except Exception as e:
784
+ logging.warning(f"Could not write dependencies to {pyproject_path}: {e}")
785
+ return False
786
+
787
+
788
+ # Per-language dependency handlers (Python only). None = not supported.
789
+ MERGE_DEPENDENCY_HANDLERS: dict[str, Callable | None] = {
790
+ "python": merge_python_dependencies,
791
+ }
792
+
793
+ WRITE_DEPENDENCY_HANDLERS: dict[str, Callable | None] = {
794
+ "python": write_python_dependencies,
795
+ }
796
+
797
+
798
+ def update_cli_metadata(
799
+ project_dir: pathlib.Path,
800
+ create_params: dict[str, Any],
801
+ *,
802
+ cli_version: str | None = None,
803
+ remove_keys: list[str] | None = None,
804
+ base_template: str | None = None,
805
+ ) -> None:
806
+ """Update specific keys in the project manifest.
807
+
808
+ A manifest that cannot be read or written is logged and skipped: every
809
+ caller writes metadata as a side effect of work that already succeeded.
810
+
811
+ Args:
812
+ project_dir: Path to the project directory
813
+ create_params: Dict of keys to update inside create_params
814
+ cli_version: If provided, update cli_version
815
+ remove_keys: List of keys to remove from the create_params section
816
+ base_template: If provided, record it at the manifest top level
817
+ """
818
+ manifest_path = project_dir / MANIFEST_FILENAME
819
+ if not manifest_path.exists():
820
+ logging.warning(f"Manifest not found: {manifest_path}")
821
+ return
822
+
823
+ try:
824
+ with open(manifest_path, encoding="utf-8") as f:
825
+ data = yaml.safe_load(f) or {}
826
+
827
+ if cli_version:
828
+ data["cli_version"] = cli_version
829
+
830
+ if base_template is not None:
831
+ data["base_template"] = base_template
832
+
833
+ if "create_params" not in data or not isinstance(data["create_params"], dict):
834
+ data["create_params"] = {}
835
+
836
+ params = data["create_params"]
837
+
838
+ for key, val in create_params.items():
839
+ params[key] = val
840
+
841
+ if remove_keys:
842
+ for key in remove_keys:
843
+ params.pop(key, None)
844
+
845
+ with open(manifest_path, "w", encoding="utf-8") as f:
846
+ yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False)
847
+ except Exception as e:
848
+ logging.warning(f"Could not update the manifest {manifest_path}: {e}")
849
+
850
+
851
+ def compare_all_files(
852
+ project_dir: pathlib.Path,
853
+ old_template_dir: pathlib.Path,
854
+ new_template_dir: pathlib.Path,
855
+ agent_directory: str = "app",
856
+ *,
857
+ merge_config: bool = False,
858
+ ) -> list[FileCompareResult]:
859
+ """Compare all files using 3-way comparison (``merge_config``: see ``three_way_compare``)."""
860
+ all_files = collect_all_files(project_dir, old_template_dir, new_template_dir)
861
+
862
+ results = []
863
+ for relative_path in sorted(all_files):
864
+ result = three_way_compare(
865
+ relative_path,
866
+ project_dir,
867
+ old_template_dir,
868
+ new_template_dir,
869
+ agent_directory,
870
+ merge_config=merge_config,
871
+ )
872
+ results.append(result)
873
+
874
+ return results
875
+
876
+
877
+ def group_results_by_action(
878
+ results: list[FileCompareResult],
879
+ ) -> dict[str, list[FileCompareResult]]:
880
+ """Group results by action type."""
881
+ groups: dict[str, list[FileCompareResult]] = {
882
+ "auto_update": [],
883
+ "preserve": [],
884
+ "skip": [],
885
+ "conflict": [],
886
+ "new": [],
887
+ "removed": [],
888
+ "merge": [],
889
+ }
890
+
891
+ for result in results:
892
+ groups[result.action].append(result)
893
+
894
+ return groups