failproofai 1.0.12-beta.1 → 1.0.12

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 (236) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +3 -3
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  30. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +3 -3
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  36. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  37. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  38. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  41. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  42. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  43. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  51. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
  55. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/chunks/_0tovk6q0miud6._.js +1 -1
  58. package/.next/standalone/.next/server/chunks/_0trp3ycqv17cl._.js +1 -1
  59. package/.next/standalone/.next/server/chunks/_1ek68lnh2daur._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4vkpxxu7._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/src_hooks_0_em82s16zfym._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08ch3staq3ejw._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bigvy9xmv2ka._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h86bl2qf0k_o._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pia5h3p8re4c._.js +1 -1
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1-ey9fgesfbc0._.js +1 -1
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15usvs-ms-0hf._.js +1 -1
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__167mipqmjprq5._.js +3 -0
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__16gb3_92zvk5x._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1avi2kypusyb_._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1r7drlqli-ado._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0b78b0j8iyv6l._.js → [root-of-the-server]__1vofkb2oq7p7e._.js} +1 -1
  73. package/.next/standalone/.next/server/chunks/ssr/_0eoura3_9gjb-._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/{_1a3tubds7s7ow._.js → _0ouu886szh6yj._.js} +4 -4
  75. package/.next/standalone/.next/server/chunks/ssr/_0pedwblqdoh58._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/_1d98fyfrcaqhy._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/ssr/_1djqs9i27e4kg._.js +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo5_vm5f.js +2 -2
  79. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9snixb5fd-._.js +1 -1
  80. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47q0xq5w._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x33jqb6._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpcz0uxur._.js +2 -2
  83. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h47jmhhd._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1i8oid1gc7s1f.js → node_modules_html-to-image_es_index_0tqc05n832v7a.js} +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/package_json_[json]_cjs_013tntikdft59._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0agdmptetetdt._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/src_hooks_0mdwgjisv4fub._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589gp4p60j._.js +1 -1
  89. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me658zqmh_._.js +1 -1
  90. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  91. package/.next/standalone/.next/server/pages/404.html +1 -1
  92. package/.next/standalone/.next/server/pages/500.html +1 -1
  93. package/.next/standalone/.next/server/preview-props.json +3 -3
  94. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  95. package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
  96. package/.next/standalone/.next/static/chunks/{1b32-afqacaen.js → 0el9n3h9_zp5m.js} +1 -1
  97. package/.next/standalone/.next/static/chunks/0k7g_8x3o6gd-.js +1 -0
  98. package/.next/standalone/.next/static/chunks/{42e8zab314654.js → 0umzct31duk5g.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{24w3x2da9sk49.js → 0wobqgosirs8p.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/12rug3jilwqzs.js +1 -0
  101. package/.next/standalone/.next/static/chunks/{2-tf6_7u623kd.js → 1kduj74-715jh.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/2701amhrbsbot.js +1 -0
  103. package/.next/standalone/.next/static/chunks/{18433qz-8njv3.js → 27cavoc8paelg.js} +1 -1
  104. package/.next/standalone/.next/static/chunks/{0_zfad4co-t2z.js → 2ve23a7keupjw.js} +1 -1
  105. package/.next/standalone/.next/static/chunks/3bv6xaoitjpnh.js +6 -0
  106. package/.next/standalone/app/actions/update-jev-config.ts +5 -3
  107. package/.next/standalone/app/settings/jev-panel.tsx +1 -1
  108. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +28 -18
  109. package/.next/standalone/fp-cloud-cli/README.md +127 -9
  110. package/.next/standalone/fp-cloud-cli/fp_cli/app.py +15 -1
  111. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +199 -34
  112. package/.next/standalone/fp-cloud-cli/fp_cli/cloud_output.py +523 -0
  113. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_cloud.py +45 -0
  114. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +3 -1
  115. package/.next/standalone/fp-cloud-cli/fp_cli/commands/account_cmds.py +171 -0
  116. package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +80 -7
  117. package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +23 -0
  118. package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +2 -0
  119. package/.next/standalone/fp-cloud-cli/fp_cli/commands/contracts_cmds.py +67 -0
  120. package/.next/standalone/fp-cloud-cli/fp_cli/commands/dashboards_cmds.py +159 -0
  121. package/.next/standalone/fp-cloud-cli/fp_cli/commands/evaluators_cmds.py +380 -0
  122. package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +14 -1
  123. package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +12 -216
  124. package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +82 -6
  125. package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +4 -1
  126. package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +17 -0
  127. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +143 -1
  128. package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +70 -0
  129. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sentiment_cmds.py +49 -0
  130. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +95 -1
  131. package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +2 -0
  132. package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +21 -1
  133. package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +14 -0
  134. package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +7 -53
  135. package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +3 -0
  136. package/.next/standalone/fp-cloud-cli/fp_cli/evaluator_spec.py +266 -0
  137. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +189 -152
  138. package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +21 -24
  139. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +124 -23
  140. package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +2 -1
  141. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +173 -22
  142. package/.next/standalone/fp-cloud-cli/tests/fake_cloud_smoke.py +312 -0
  143. package/.next/standalone/fp-cloud-cli/tests/test_cloud_account.py +125 -0
  144. package/.next/standalone/fp-cloud-cli/tests/test_cloud_agent.py +253 -0
  145. package/.next/standalone/fp-cloud-cli/tests/test_cloud_human_views.py +287 -0
  146. package/.next/standalone/fp-cloud-cli/tests/test_cloud_jev.py +32 -197
  147. package/.next/standalone/fp-cloud-cli/tests/test_cloud_observability.py +116 -0
  148. package/.next/standalone/fp-cloud-cli/tests/test_cloud_transport.py +141 -0
  149. package/.next/standalone/fp-cloud-cli/tests/test_commands.py +3 -3
  150. package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +3 -3
  151. package/.next/standalone/fp-cloud-cli/tests/test_evaluator_authoring.py +449 -0
  152. package/.next/standalone/fp-cloud-cli/tests/test_facets.py +1 -1
  153. package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +15 -0
  154. package/.next/standalone/fp-cloud-cli/tests/test_operator.py +1 -1
  155. package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +1 -1
  156. package/.next/standalone/fp-cloud-cli/tests/test_output.py +37 -37
  157. package/.next/standalone/fp-cloud-cli/tests/test_round4_behaviour.py +144 -0
  158. package/.next/standalone/fp-cloud-cli/tests/test_screen_language.py +80 -0
  159. package/.next/standalone/fp-cloud-cli/tests/test_session_reevaluate.py +33 -0
  160. package/.next/standalone/fp-cloud-cli/tests/test_staging_parity.py +198 -0
  161. package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +1 -1
  162. package/.next/standalone/hermes-plugin/__init__.py +11 -0
  163. package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
  164. package/.next/standalone/package.json +11 -10
  165. package/.next/standalone/sdk/python/uv.lock +31 -8
  166. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +68 -176
  167. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +2 -2
  168. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +4 -4
  169. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +1 -1
  170. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +7 -7
  171. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +1 -1
  172. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +7 -7
  173. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +1 -1
  174. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +98 -115
  175. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +3 -3
  176. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +4 -4
  177. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +1 -1
  178. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +4 -4
  179. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +1 -1
  180. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +149 -171
  181. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +2 -2
  182. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +4 -4
  183. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +1 -1
  184. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +25 -25
  185. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +1 -1
  186. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +7 -7
  187. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +1 -1
  188. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +10 -10
  189. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +1 -1
  190. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +7 -7
  191. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +1 -1
  192. package/.next/standalone/server.js +1 -1
  193. package/dist/cli.mjs +1760 -638
  194. package/dist/worker.mjs +426 -76
  195. package/hermes-plugin/__init__.py +11 -0
  196. package/hermes-plugin/plugin.yaml +1 -1
  197. package/package.json +11 -10
  198. package/src/audit/open-browser.ts +24 -7
  199. package/src/hooks/agent-roster.ts +9 -1
  200. package/src/hooks/agent-skill.ts +206 -0
  201. package/src/hooks/cloud-connection.ts +27 -51
  202. package/src/hooks/cloud-introspect.ts +5 -2
  203. package/src/hooks/cloud-login.ts +248 -0
  204. package/src/hooks/cloud-managed-policies.ts +54 -4
  205. package/src/hooks/codex-system-hooks.ts +10 -4
  206. package/src/hooks/config-status.ts +9 -3
  207. package/src/hooks/configure-wizard.ts +356 -32
  208. package/src/hooks/delivery-health.ts +47 -2
  209. package/src/hooks/effective-reviewers.ts +14 -0
  210. package/src/hooks/fp-config.ts +19 -0
  211. package/src/hooks/fp-home.ts +1 -1
  212. package/src/hooks/handler.ts +43 -5
  213. package/src/hooks/hook-activity-store.ts +7 -1
  214. package/src/hooks/integrations.ts +18 -3
  215. package/src/hooks/jev-activity.ts +117 -1
  216. package/src/hooks/jev-cli.ts +7 -9
  217. package/src/hooks/manager.ts +80 -46
  218. package/src/hooks/policy-evaluator.ts +17 -0
  219. package/src/hooks/policy-reviewability.ts +4 -2
  220. package/src/hooks/semantic/cloud-jev.ts +219 -28
  221. package/src/hooks/semantic/combine.ts +26 -1
  222. package/src/hooks/semantic/intent.ts +127 -24
  223. package/src/hooks/semantic/jev-client.ts +1 -1
  224. package/src/hooks/semantic/jev-config.ts +6 -27
  225. package/src/hooks/semantic/jev-review.ts +111 -4
  226. package/src/hooks/tui.ts +280 -0
  227. package/src/hooks/uninstall-cli.ts +12 -1
  228. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0588st-tpiuia._.js +0 -3
  229. package/.next/standalone/.next/server/chunks/ssr/_128ebxr83nk6g._.js +0 -3
  230. package/.next/standalone/.next/static/chunks/0jutamwqz8pg5.js +0 -1
  231. package/.next/standalone/.next/static/chunks/114hbolvrv94e.js +0 -1
  232. package/.next/standalone/.next/static/chunks/1p-hsi58i-6h0.js +0 -6
  233. package/.next/standalone/.next/static/chunks/3-pku9qwv1yfu.js +0 -1
  234. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_buildManifest.js +0 -0
  235. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_clientMiddlewareManifest.js +0 -0
  236. /package/.next/standalone/.next/static/{xq8UgF5yFxty1VWxY2hyn → D7LY1YSrw96ZxKSpxxrFw}/_ssgManifest.js +0 -0
@@ -0,0 +1,523 @@
1
+ """Human views for the additive Cloud CLI surface.
2
+
3
+ JSON output never passes through this module. Tables stay tables at narrow widths;
4
+ long prose wraps beside grey labels rather than becoming FIELD / VALUE blocks.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Any, Optional
11
+
12
+ from rich.text import Text
13
+
14
+ from . import output, theme
15
+
16
+
17
+ def begin(title: str) -> None:
18
+ output.screen_header()
19
+ output._stdout.print(Text(title.upper(), style="bold"))
20
+
21
+
22
+ def value(data: Any) -> str:
23
+ """Human text, never a Python repr or serialized JSON container."""
24
+ if data is None:
25
+ return "—"
26
+ if isinstance(data, bool):
27
+ return "yes" if data else "no"
28
+ if isinstance(data, dict):
29
+ return "; ".join(f"{label(k)}: {value(v)}" for k, v in data.items())
30
+ if isinstance(data, (list, tuple)):
31
+ return ", ".join(value(v) for v in data) or "none"
32
+ return str(data)
33
+
34
+
35
+ def label(key: Any) -> str:
36
+ return str(key).replace("_", " ").removesuffix(" source")
37
+
38
+
39
+ def state(enabled: Any, *, on="enabled", off="disabled") -> Text:
40
+ if enabled is None:
41
+ return Text("unknown", style=theme.LABEL)
42
+ return Text("● " + on, style=theme.ACCENT) if enabled else Text("○ " + off, style=theme.LABEL)
43
+
44
+
45
+ def kv(pairs) -> None:
46
+ rows = [(label(k), v) for k, v in pairs if v is not None]
47
+ if not rows:
48
+ return
49
+ table = output.Table.grid(padding=(0, 2, 0, 0), pad_edge=False)
50
+ table.add_column(width=max(len(k) for k, _ in rows) + 2, style=theme.LABEL)
51
+ table.add_column(ratio=1, overflow="fold")
52
+ for key, item in rows:
53
+ table.add_row(Text(" " + key, style=theme.LABEL), item if isinstance(item, Text) else Text(value(item)))
54
+ output._stdout.print(table)
55
+
56
+
57
+ def line(text: str) -> None:
58
+ output._stdout.print(Text(" " + text))
59
+
60
+
61
+ def next_command(text: str) -> None:
62
+ output._stdout.print(Text(" " + text, style=theme.PINK))
63
+
64
+
65
+ def empty(message: str, command: str) -> None:
66
+ line(message)
67
+ next_command(command)
68
+
69
+
70
+ def table(columns, rows, *, empty_message: str, command: str) -> None:
71
+ if not rows:
72
+ empty(empty_message, command)
73
+ return
74
+ grid = output.Table(box=None, padding=(0, 2, 0, 0), pad_edge=False, header_style=theme.LABEL)
75
+ for column in columns:
76
+ grid.add_column(column, overflow="fold")
77
+ for row in rows:
78
+ grid.add_row(*[v if isinstance(v, Text) else Text(value(v)) for v in row])
79
+ output._stdout.print(grid)
80
+
81
+
82
+ def collection(data: Any, *keys: str) -> list:
83
+ if isinstance(data, list):
84
+ return data
85
+ if isinstance(data, dict):
86
+ for key in keys:
87
+ if isinstance(data.get(key), list):
88
+ return data[key]
89
+ return []
90
+
91
+
92
+ def detail(data: Any, *, skip=()) -> None:
93
+ """Render nested API detail as named sections, not opaque structures."""
94
+ if isinstance(data, dict):
95
+ scalar = [(k, v) for k, v in data.items() if k not in skip and not isinstance(v, (dict, list))]
96
+ kv(scalar)
97
+ for key, item in data.items():
98
+ if key in skip or not isinstance(item, (dict, list)):
99
+ continue
100
+ if not item:
101
+ kv([(key, "none")])
102
+ elif isinstance(item, dict):
103
+ line(label(key))
104
+ detail(item)
105
+ elif all(not isinstance(v, (dict, list)) for v in item):
106
+ kv([(key, item)])
107
+ else:
108
+ line(label(key))
109
+ for row in item:
110
+ detail(row)
111
+ output._stdout.print()
112
+ elif isinstance(data, list):
113
+ for row in data:
114
+ detail(row)
115
+ elif data is not None:
116
+ line(value(data))
117
+
118
+
119
+ def envelope(row: dict) -> dict:
120
+ try:
121
+ source = json.loads(row.get("evaluator_source") or "")
122
+ except (ValueError, TypeError):
123
+ return {}
124
+ return source if isinstance(source, dict) else {}
125
+
126
+
127
+ def evaluator_kind(row: dict) -> str:
128
+ source = envelope(row)
129
+ if source.get("kind") == "judge":
130
+ return "LLM judge"
131
+ if source.get("kind") == "jev":
132
+ return "Jev " + str(source.get("type", ""))
133
+ return "code"
134
+
135
+
136
+ def version(raw: Any) -> str:
137
+ text = value(raw)
138
+ return text if text.startswith("v") else "v" + text
139
+
140
+
141
+ def evaluator(row: dict, title="Evaluator") -> None:
142
+ begin(title)
143
+ kv([
144
+ ("key", row.get("eval_key")), ("version", row.get("eval_version")),
145
+ ("name", row.get("display_name")), ("kind", evaluator_kind(row)),
146
+ ("enabled", state(row.get("enabled", True))), ("condition", row.get("condition_source") or "always"),
147
+ ("result kind", row.get("result_kind", "score")), ("labels", row.get("labels", [])),
148
+ ("id", row.get("id")),
149
+ ])
150
+ source = envelope(row)
151
+ if source.get("kind") == "judge":
152
+ kv([("criteria", source.get("criteria")),
153
+ ("threshold", source["threshold"] if source.get("threshold") is not None else "server default")])
154
+ for i, step in enumerate(source.get("steps") or [], 1):
155
+ kv([(f"step {i}", step)])
156
+ elif source.get("kind") == "jev":
157
+ kv([("instructions", source.get("instructions")),
158
+ ("threshold", source["threshold"] if source.get("threshold") is not None else "server default")])
159
+ criteria = source.get("criteria")
160
+ if isinstance(criteria, dict):
161
+ kv([(str(k), v) for k, v in criteria.items()])
162
+ elif isinstance(criteria, list):
163
+ for i, level in enumerate(criteria, 1):
164
+ kv([(f"level {i}", level)])
165
+ else:
166
+ kv([("source", row.get("evaluator_source")), ("timeout", row.get("timeout_seconds"))])
167
+
168
+
169
+ def evaluators(data: Any) -> None:
170
+ begin("Evaluators")
171
+ items = collection(data, "definitions")
172
+ table(["key", "version", "kind", "enabled", "labels"], [
173
+ [r.get("eval_key"), r.get("eval_version"), evaluator_kind(r),
174
+ state(r.get("enabled", True)), r.get("labels", [])] for r in items
175
+ ], empty_message="no evaluators yet", command="fp evaluators create --help")
176
+
177
+
178
+ def deployed(created: dict, predecessors: list) -> None:
179
+ begin("Evaluator deployed")
180
+ text = Text(f" deployed {created.get('eval_key', 'evaluator')} {version(created.get('eval_version'))} ")
181
+ text.append_text(state(created.get("enabled", True)))
182
+ if predecessors:
183
+ text.append(" · disabled " + ", ".join(version(r.get("eval_version")) for r in predecessors))
184
+ output._stdout.print(text)
185
+ kv([("id", created.get("id"))])
186
+
187
+
188
+ def score_of(result: Any) -> Any:
189
+ if not isinstance(result, dict):
190
+ return None
191
+ score = result.get("score")
192
+ if isinstance(score, dict):
193
+ return score.get("value")
194
+ return score if score is not None else result.get("scores")
195
+
196
+
197
+ def dry_run(data: dict) -> None:
198
+ begin("Evaluator dry-run")
199
+ runs = collection(data, "runs")
200
+ if not runs and "ok" in data:
201
+ runs = [data]
202
+ if not runs:
203
+ detail(data)
204
+ return
205
+ for run in runs:
206
+ result = run.get("result") or {}
207
+ skipped = run.get("applicable") is False
208
+ status = "skipped" if skipped else "passed" if run.get("ok") else "failed"
209
+ glyph = "○" if skipped else "✓" if run.get("ok") else "✕"
210
+ line(f"{glyph} {run.get('session_id') or 'compile only'} · {status}")
211
+ kv([
212
+ ("score", score_of(result)), ("reasoning", result.get("reasoning") if isinstance(result, dict) else None),
213
+ ("error", run.get("error_message") or run.get("error_kind")),
214
+ ("duration ms", run.get("duration_ms")),
215
+ ])
216
+ summary = data.get("summary")
217
+ if isinstance(summary, dict):
218
+ line(" · ".join(f"{summary.get(k, 0)} {k}" for k in ("succeeded", "failed", "skipped")))
219
+
220
+
221
+ def roles(data: Any, *, listing: bool) -> None:
222
+ begin("Roles" if listing else "Role")
223
+ if listing:
224
+ table(["name", "built-in", "users", "permissions"], [
225
+ [r.get("name"), state(r.get("is_builtin", False), on="yes", off="no"),
226
+ r.get("user_count", 0), len(r.get("permissions") or [])]
227
+ for r in collection(data, "permission_sets", "roles")
228
+ ], empty_message="no custom roles yet", command="fp roles create --name reviewer --permission events:read")
229
+ elif isinstance(data, dict):
230
+ kv([("name", data.get("name")), ("built-in", data.get("is_builtin")),
231
+ ("users", data.get("user_count")), ("description", data.get("description"))])
232
+ kv([("permissions", data.get("permissions", []))])
233
+
234
+
235
+ def dashboards(data: Any, *, listing=False, tile_view=False, dashboard_id=None) -> None:
236
+ if tile_view:
237
+ begin("Tiles")
238
+ items = collection(data, "tiles") if listing else [data]
239
+ table(["title", "chart", "query", "id"], [
240
+ [r.get("title") or "untitled", r.get("chart_type"), r.get("query_id"), r.get("id")]
241
+ for r in items if isinstance(r, dict)
242
+ ], empty_message="no tiles yet",
243
+ command=f"fp dashboards tiles {dashboard_id or '<id>'} add --query <query-id> --chart table")
244
+ return
245
+ begin("Dashboards" if listing else "Dashboard")
246
+ if listing:
247
+ table(["name", "tiles", "description", "id"], [
248
+ [r.get("name"), r.get("tile_count", 0), r.get("description") or "—", r.get("id")]
249
+ for r in collection(data, "dashboards")
250
+ ], empty_message="no dashboards yet", command="fp dashboards create --name Quality")
251
+ elif isinstance(data, dict):
252
+ row = data.get("dashboard", data)
253
+ kv([("name", row.get("name")), ("description", row.get("description")), ("id", row.get("id"))])
254
+ if row.get("definition"):
255
+ line("filters and display")
256
+ detail(row["definition"])
257
+ if "tiles" in data:
258
+ dashboards(data["tiles"], listing=True, tile_view=True, dashboard_id=row.get("id"))
259
+
260
+
261
+ def query_result(data: dict, title="Query result") -> None:
262
+ begin(title)
263
+ columns = [c.get("name", "") if isinstance(c, dict) else str(c) for c in data.get("columns", [])]
264
+ table(columns, data.get("rows", []), empty_message="query returned no rows", command="fp query schema")
265
+ kv([("elapsed ms", data.get("elapsed_ms")), ("truncated", data.get("truncated"))])
266
+
267
+
268
+ def sentiment(data: Any, mode: str) -> None:
269
+ begin("Sentiment" if not mode else "Sentiment " + mode)
270
+ mode = "session" if mode.startswith("sessions/") else mode
271
+ items = collection(data, mode, "items", "messages", "sessions")
272
+ if isinstance(data, list) or items or isinstance(data, dict) and any(k in data for k in (mode, "items")):
273
+ columns = ["session", "sentiment", "score", "message"] if mode == "messages" else ["session", "sentiment", "negative", "messages"]
274
+ def strongest(row):
275
+ scores = row.get("scores") or {}
276
+ numeric = [(k, v) for k, v in scores.items() if isinstance(v, (int, float))]
277
+ return max(numeric, key=lambda pair: pair[1]) if numeric else ("—", "—")
278
+ table(columns, [
279
+ [r.get("session_id"), r.get("sentiment") or r.get("label") or r.get("feeling") or strongest(r)[0],
280
+ r.get("score", r.get("negative", r.get("negative_score", strongest(r)[1]))),
281
+ r.get("text") or r.get("message") if mode == "messages" else r.get("message_count", r.get("messages_count"))]
282
+ for r in items if isinstance(r, dict)
283
+ ], empty_message="no sentiment results in this range", command="fp settings set sentiment.enabled --json-value true")
284
+ else:
285
+ detail(data)
286
+ if not data:
287
+ empty("no sentiment results yet", "fp settings set sentiment.enabled --json-value true")
288
+
289
+
290
+ def plan(data: dict) -> None:
291
+ begin("Plan")
292
+ offers = data.get("offers", {})
293
+ items = collection(offers, "offers")
294
+ table(["offer", "tier", "status", "expires"], [
295
+ [r.get("offer_key", r.get("key")), r.get("tier", r.get("plan")), r.get("status"),
296
+ r.get("expires_at")] for r in items if isinstance(r, dict)
297
+ ], empty_message="no offers available", command="fp usage")
298
+ trial = data.get("team_trial", {})
299
+ line("team trial")
300
+ if trial:
301
+ detail(trial)
302
+ else:
303
+ line("no active team trial")
304
+
305
+
306
+ def brief(data: dict) -> None:
307
+ begin("Home brief")
308
+ for key in ("greeting", "summary"):
309
+ if data.get(key):
310
+ line(str(data[key]))
311
+ for block in data.get("blocks", []):
312
+ artifact(block)
313
+ if not data.get("blocks"):
314
+ empty("no brief blocks yet", 'fp agent ask "Summarize my recent sessions"')
315
+
316
+
317
+ def artifact(block: dict) -> None:
318
+ data = block.get("result")
319
+ if isinstance(data, dict):
320
+ query_result(data, block.get("title", "Artifact"))
321
+ else:
322
+ begin(block.get("title", "Home"))
323
+ for key in ("text", "note"):
324
+ if block.get(key):
325
+ line(str(block[key]))
326
+ for key in ("items", "sessions"):
327
+ for item in block.get(key, []):
328
+ detail(item)
329
+
330
+
331
+ def resource(path: str, method: str, data: Any, body=None, params=None) -> None:
332
+ """Request-specific human views used by shared mutation orchestrators."""
333
+ body = body if isinstance(body, dict) else {}
334
+ row = data if isinstance(data, dict) else {}
335
+ parts = path.removeprefix("/api/").split("/")
336
+ family = parts[0]
337
+ if family == "permission-sets":
338
+ if method == "DELETE":
339
+ begin("Role deleted")
340
+ line(f"deleted {parts[-1]} · members keep their permissions")
341
+ else:
342
+ roles({**body, **row} if method != "GET" else data, listing=len(parts) == 1 and method == "GET")
343
+ elif family == "dashboards":
344
+ if method == "DELETE":
345
+ begin("Tile removed" if "tiles" in parts else "Dashboard deleted")
346
+ kv([("id", parts[-1])])
347
+ elif parts[-1] == "layout":
348
+ begin("Tile layout updated")
349
+ kv([("tiles updated", row.get("updated", len(body.get("tiles", []))))])
350
+ else:
351
+ dashboards(data, listing=method == "GET" and (len(parts) == 1 or parts[-1] == "tiles"),
352
+ tile_view="tiles" in parts, dashboard_id=parts[1] if len(parts) > 1 else None)
353
+ elif family == "otel" or family == "settings" and parts[-1] == "otel.ingest_enabled":
354
+ begin("OTEL")
355
+ kv([("ingest", state(row.get("enabled", row.get("value", body.get("value"))))),
356
+ ("events 24h", row.get("events_24h")), ("last received", row.get("last_received_at")),
357
+ ("content missing", row.get("content_missing"))])
358
+ services = collection(row, "services")
359
+ table(["service", "events", "last received"], [
360
+ [r.get("name", r.get("service_name")), r.get("event_count", r.get("events")), r.get("last_received_at")]
361
+ if isinstance(r, dict) else [r, "—", "—"] for r in services
362
+ ], empty_message="no OTEL services yet", command="fp otel enable")
363
+ elif family == "integrations":
364
+ begin("Slack")
365
+ connection = row.get("integration", row.get("connection", row))
366
+ connection = connection if isinstance(connection, dict) else {}
367
+ kv([("connection", state(method != "DELETE" and connection.get("connected", connection.get("installed", bool(connection))),
368
+ on="connected", off="disconnected")),
369
+ ("workspace", connection.get("team_name", connection.get("workspace_name"))),
370
+ ("channel", connection.get("channel_name")), ("connected at", connection.get("created_at"))])
371
+ if method == "DELETE":
372
+ line("Slack disconnected")
373
+ elif not connection:
374
+ line("Connect Slack from your Cloud integration settings.")
375
+ elif family == "settings":
376
+ begin("Context windows")
377
+ if method == "GET":
378
+ table(["model", "tokens", "updated"], [
379
+ [r.get("model_id"), r.get("context_window"), r.get("updated_at")]
380
+ for r in collection(data, "model_context_windows")
381
+ ], empty_message="no model overrides yet", command="fp settings context-windows set <model> <tokens>")
382
+ else:
383
+ kv([("model", row.get("model_id", body.get("model_id", (params or {}).get("model")))),
384
+ ("tokens", row.get("context_window", body.get("context_window")))])
385
+ if method == "DELETE":
386
+ line("override removed")
387
+ elif family == "offers":
388
+ begin("Offer claimed")
389
+ kv([("offer", body.get("offer_key"))])
390
+ detail(data)
391
+ elif family == "users":
392
+ begin("Member deleted" if method == "DELETE" else "Member defaults")
393
+ if method == "DELETE":
394
+ kv([("id", parts[-1])])
395
+ else:
396
+ detail(data)
397
+ elif family == "orgs":
398
+ begin("Organization created")
399
+ kv([("name", row.get("name", body.get("name"))), ("slug", row.get("slug", body.get("slug"))),
400
+ ("id", row.get("id")), ("plan", row.get("plan"))])
401
+ next_command(f"fp orgs switch {row.get('slug', body.get('slug', '<slug>'))}")
402
+ elif family == "usage":
403
+ begin("Usage windows")
404
+ table(["from", "to", "events", "evaluations"], [
405
+ [r.get("from", r.get("starts_at")), r.get("to", r.get("ends_at")),
406
+ r.get("events_ingested", (r.get("usage") or {}).get("events_ingested")),
407
+ r.get("evaluations", (r.get("usage") or {}).get("evaluations"))]
408
+ for r in collection(data, "windows")
409
+ ], empty_message="no historical usage windows yet", command="fp usage")
410
+ elif family == "alerts":
411
+ begin("Alert recipients" if parts[-1] == "recipients" else "Alert issues")
412
+ items = collection(data, "recipients", "issues")
413
+ if method != "GET":
414
+ items = [data]
415
+ if parts[-1] == "recipients":
416
+ table(["recipient", "channel"], [
417
+ [r.get("email", r.get("name")), r.get("channel", "email")] if isinstance(r, dict) else [r, "email"] for r in items
418
+ ], empty_message="no recipients available", command="fp users list")
419
+ else:
420
+ table(["title", "status", "severity", "id"], [
421
+ [r.get("title"), r.get("state", r.get("status")), r.get("severity"), r.get("id")] for r in items
422
+ ], empty_message="no issues for this alert", command=f"fp alerts issues {parts[1]} --create --title <title>")
423
+ elif family == "audits":
424
+ begin("Agent contracts")
425
+ if method == "GET" and len(parts) == 2:
426
+ table(["agent", "enabled", "purpose", "id"], [
427
+ [r.get("agent_id"), state(r.get("enabled", True)), r.get("purpose"), r.get("id")]
428
+ for r in collection(data, "contracts")
429
+ ], empty_message="no agent contracts yet", command="fp audits contracts create --agent-id <agent> --body @agent.md")
430
+ else:
431
+ kv([("agent", row.get("agent_id", body.get("agent_id"))), ("enabled", state(row.get("enabled", True))),
432
+ ("id", row.get("id")), ("body", row.get("body", body.get("body")))])
433
+ else:
434
+ begin(label(family))
435
+ detail(data)
436
+
437
+
438
+ def render(data: Any, title: str, *, view: Optional[str] = None, body=None, method="GET", params=None) -> None:
439
+ """Explicit views for commands without the shared resource orchestrator."""
440
+ if view and view.startswith("/api/"):
441
+ resource(view, method, data, body, params)
442
+ elif title == "Evaluators":
443
+ evaluators(data)
444
+ elif title in ("Evaluator", "Evaluator created", "Evaluator labels"):
445
+ evaluator(data, title)
446
+ elif title == "Evaluator draft":
447
+ evaluator(data.get("spec", {}), title)
448
+ kv([("file", data.get("out"))])
449
+ next_command(f"fp evaluators create --file {data.get('out')}")
450
+ elif title == "Evaluator dry-run":
451
+ dry_run(data)
452
+ elif title == "Instances":
453
+ begin(title)
454
+ table(["name", "kind", "enabled", "evaluators", "queued", "id"], [
455
+ [r.get("name"), r.get("kind"), state(r.get("enabled", True)), r.get("eval_count", 0), r.get("queued", 0), r.get("id")]
456
+ for r in collection(data, "instances")
457
+ ], empty_message="no managed instances available", command="Check your Cloud managed-evaluator deployment.")
458
+ elif title == "Schema profile":
459
+ begin(title)
460
+ kv([(k, data.get(k)) for k in ("window_days", "sampled_events")] if isinstance(data, dict) else [])
461
+ for event in collection(data, "event_types"):
462
+ line(f"{event.get('event_type')} · {event.get('count', 0)} events")
463
+ table(["payload key", "type", "presence"], [
464
+ [r.get("key"), r.get("type"), r.get("presence")] for r in event.get("keys", [])
465
+ ], empty_message="no sampled payload keys", command="fp events --since 24h")
466
+ if not collection(data, "event_types"):
467
+ empty("no sampled event types yet", "fp events --since 24h")
468
+ elif title == "Evaluation jobs":
469
+ begin(title)
470
+ table(["session", "evaluator", "status", "id"], [
471
+ [r.get("session_id"), r.get("eval_key"), r.get("status"), r.get("id")]
472
+ for r in collection(data, "jobs", "evaluation_jobs")
473
+ ], empty_message="no evaluation jobs", command="fp sessions re-evaluate <session-id>")
474
+ elif title == "Evaluation backfill":
475
+ begin(title)
476
+ detail(data)
477
+ next_command("fp evaluators jobs")
478
+ elif title == "Session re-evaluation":
479
+ begin("Re-evaluate")
480
+ count = data.get("v2_assignments_queued", data.get("queued_count"))
481
+ session_id = data.get("session_id") or body
482
+ if count is not None:
483
+ line(f"queued {count} evaluation{'s' if count != 1 else ''} for {session_id}")
484
+ else:
485
+ line(f"re-evaluation queued for {session_id}" if data.get("queued") or data.get("status") == "queued" else "No evaluations queued.")
486
+ elif title == "Session":
487
+ begin(title)
488
+ row = data.get("overview", data)
489
+ kv([(k, row.get(k)) for k in ("session_id", "agent_id", "environment", "status", "event_count", "started_at", "last_event_at")])
490
+ begin("Timeline")
491
+ table(["time", "type", "agent", "summary"], [
492
+ [r.get("ts"), r.get("event_type"), r.get("agent_id"), r.get("summary")]
493
+ for r in collection(data.get("timeline", data), "events")
494
+ ], empty_message="no events in this session", command="fp events --session-id " + str(row.get("session_id", "<id>")))
495
+ elif title == "Evaluation history":
496
+ begin(title)
497
+ table(["evaluator", "version", "status", "score", "finished"], [
498
+ [r.get("eval_key"), r.get("eval_version"), r.get("status"), score_of(r), r.get("finished_at")]
499
+ for r in collection(data, "evaluations", "history", "items")
500
+ ], empty_message="no evaluations for this session", command="fp sessions re-evaluate <session-id>")
501
+ elif title == "Sentiment":
502
+ sentiment(data, view or "")
503
+ elif title == "Plan":
504
+ plan(data)
505
+ elif title == "Composed query":
506
+ begin(title)
507
+ kv([("SQL", data.get("sql"))])
508
+ if data.get("result"):
509
+ query_result(data["result"])
510
+ elif title in ("Query exported", "Session exported"):
511
+ begin(title)
512
+ kv([("file", data.get("out")), ("format", data.get("format"))])
513
+ elif title in ("Policy draft", "Policy backtest", "Policy candidacy"):
514
+ begin(title)
515
+ detail(data)
516
+ if title == "Policy draft":
517
+ line("Draft only — nothing published or deployed.")
518
+ elif title == "Event latency":
519
+ begin(title)
520
+ detail(data)
521
+ else:
522
+ begin(title)
523
+ detail(data)
@@ -0,0 +1,45 @@
1
+ """Shared output and auth for the additive Cloud command surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from .. import _click_compat as click
9
+ from .. import output
10
+ from .. import cloud_output
11
+ from .._context import deny_in_key_mode
12
+ from ..errors import KeyModeUnsupportedError
13
+
14
+
15
+ def session_only(state, command: str) -> None:
16
+ try:
17
+ deny_in_key_mode(state, command, "there is no /v1 route")
18
+ except KeyModeUnsupportedError:
19
+ raise KeyModeUnsupportedError(
20
+ f"`fp {command}` needs a signed-in session: run `fp login`"
21
+ ) from None
22
+
23
+
24
+ def rows(data: Any, key: str) -> list:
25
+ if isinstance(data, list):
26
+ return data
27
+ if isinstance(data, dict) and isinstance(data.get(key), list):
28
+ return data[key]
29
+ raise click.UsageError(f"Cloud returned an invalid {key} collection")
30
+
31
+
32
+ def emit(state, data: Any, title: str, **render_options) -> None:
33
+ if state is not None and state.json:
34
+ output.emit_json(data)
35
+ return
36
+ cloud_output.render(data, title, **render_options)
37
+
38
+
39
+ def write_file(path: str, text: str) -> None:
40
+ try:
41
+ # Exclusive creation: never silently replace a user's source or export.
42
+ with Path(path).open("x", encoding="utf-8") as dest:
43
+ dest.write(text)
44
+ except OSError as exc:
45
+ raise click.BadParameter(f"cannot write '{path}': {exc}", param_hint="--out") from exc
@@ -89,7 +89,9 @@ def read_text_arg(path: str, *, flag: str = "--file") -> str:
89
89
  if path == "-":
90
90
  return sys.stdin.read()
91
91
  try:
92
- return Path(path).read_text()
92
+ return Path(path).read_text(encoding="utf-8")
93
+ except UnicodeError:
94
+ raise click.BadParameter(f"cannot read '{path}': expected UTF-8 text", param_hint=flag) from None
93
95
  except OSError as exc:
94
96
  raise click.BadParameter(
95
97
  f"cannot read '{path}': {exc.strerror or exc}", param_hint=flag