failproofai 1.0.1 → 1.0.2-beta.1

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 (390) 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 +6 -6
  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 +2 -2
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
  24. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js +5 -4
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -2
  29. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +4 -3
  31. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/logout/route.js +4 -4
  33. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -4
  35. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  38. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  39. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/index.html +1 -1
  42. package/.next/standalone/.next/server/app/index.rsc +15 -15
  43. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  44. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  45. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  46. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/page.js +2 -2
  48. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +66 -22
  51. package/.next/standalone/.next/server/app/policies/page.js +5 -4
  52. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
  56. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  64. package/.next/standalone/.next/server/app/projects/page.js +2 -2
  65. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  68. package/.next/standalone/.next/server/app/settings/page.js +1 -1
  69. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/chunks/{[externals]__1qzqcio._.js → [externals]__0e32-7c._.js} +1 -1
  72. package/.next/standalone/.next/server/chunks/[externals]__1s462yl._.js +3 -0
  73. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0neqlu-._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +3 -0
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t9oqnn._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -6
  84. package/.next/standalone/.next/server/chunks/lib_telemetry-id_ts_1k8gj9o._.js +3 -0
  85. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_09d5c79._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js} +1 -1
  86. package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1d2g73o._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.js} +1 -1
  87. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/src_hooks_0xbw3bu._.js +12 -0
  89. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
  93. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0spkm68._.js → [root-of-the-server]__07gm3zl._.js} +21 -20
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0l4748x._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
  99. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__192nhvh._.js +5 -0
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
  102. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/ssr/_0bwu160._.js +23 -0
  104. package/.next/standalone/.next/server/chunks/ssr/{_0u5zauv._.js → _0ns6k3a._.js} +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +13 -13
  108. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/src_hooks_08o7bqo._.js +10 -0
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  114. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  115. package/.next/standalone/.next/server/middleware.js +1 -1
  116. package/.next/standalone/.next/server/pages/404.html +1 -1
  117. package/.next/standalone/.next/server/pages/500.html +1 -1
  118. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.json +71 -27
  120. package/.next/standalone/.next/static/chunks/{3c808qrfcq0im.js → 01u1cf8js2yd6.js} +1 -1
  121. package/.next/standalone/.next/static/chunks/{0k5050ezjvv49.js → 0izgu7nqh2zp-.js} +1 -1
  122. package/.next/standalone/.next/static/chunks/0u4-org_e7zde.js +1 -0
  123. package/.next/standalone/.next/static/chunks/1mfzsja6ftim5.css +2 -0
  124. package/.next/standalone/.next/static/chunks/{2zpp1we2jv6tb.js → 1tynnrrh711lr.js} +1 -1
  125. package/.next/standalone/.next/static/chunks/{1ide1wl2f7zi1.js → 22rqvlr7p_1mv.js} +13 -13
  126. package/.next/standalone/.next/static/chunks/{31s1smp-g7gks.js → 2whxqyskpirbk.js} +1 -1
  127. package/.next/standalone/.next/static/chunks/31mwk8k0q3_3g.js +6 -0
  128. package/.next/standalone/.next/static/chunks/3ms6gxfm82t6s.js +1 -0
  129. package/.next/standalone/.next/static/chunks/{37fcpc36x9gpe.js → 3tbbs_d8xrvyp.js} +1 -1
  130. package/.next/standalone/.next/static/chunks/43nlc236oo898.js +1 -0
  131. package/.next/standalone/SECURITY.md +5 -4
  132. package/.next/standalone/app/actions/get-hooks-config.ts +108 -17
  133. package/.next/standalone/app/actions/pack-actions.ts +163 -0
  134. package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
  135. package/.next/standalone/app/audit/_components/share-templates.ts +13 -13
  136. package/.next/standalone/app/policies/hooks-client.tsx +320 -15
  137. package/.next/standalone/components/navbar.tsx +2 -28
  138. package/.next/standalone/components/reach-developers.tsx +8 -1
  139. package/.next/standalone/docs-old/agenteye/python-sdk-skill.mdx +1 -1
  140. package/.next/standalone/docs-old/agenteye/python-sdk.mdx +59 -35
  141. package/.next/standalone/docs-old/i18n/README.ar.md +1 -0
  142. package/.next/standalone/docs-old/i18n/README.de.md +1 -0
  143. package/.next/standalone/docs-old/i18n/README.es.md +1 -0
  144. package/.next/standalone/docs-old/i18n/README.fr.md +1 -0
  145. package/.next/standalone/docs-old/i18n/README.he.md +1 -0
  146. package/.next/standalone/docs-old/i18n/README.hi.md +1 -0
  147. package/.next/standalone/docs-old/i18n/README.it.md +1 -0
  148. package/.next/standalone/docs-old/i18n/README.ja.md +1 -0
  149. package/.next/standalone/docs-old/i18n/README.ko.md +1 -0
  150. package/.next/standalone/docs-old/i18n/README.pt-br.md +1 -0
  151. package/.next/standalone/docs-old/i18n/README.ru.md +1 -0
  152. package/.next/standalone/docs-old/i18n/README.tr.md +1 -0
  153. package/.next/standalone/docs-old/i18n/README.vi.md +1 -0
  154. package/.next/standalone/docs-old/i18n/README.zh.md +1 -0
  155. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +431 -0
  156. package/.next/standalone/fp-cloud-cli/LICENSE +42 -0
  157. package/.next/standalone/fp-cloud-cli/MANIFEST.in +10 -0
  158. package/.next/standalone/fp-cloud-cli/README.md +259 -0
  159. package/.next/standalone/fp-cloud-cli/fp_cli/__init__.py +10 -0
  160. package/.next/standalone/fp-cloud-cli/fp_cli/__main__.py +4 -0
  161. package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +64 -0
  162. package/.next/standalone/fp-cloud-cli/fp_cli/_context.py +332 -0
  163. package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -0
  164. package/.next/standalone/fp-cloud-cli/fp_cli/analytics.py +432 -0
  165. package/.next/standalone/fp-cloud-cli/fp_cli/analytics_config.py +77 -0
  166. package/.next/standalone/fp-cloud-cli/fp_cli/analytics_registry.py +83 -0
  167. package/.next/standalone/fp-cloud-cli/fp_cli/app.py +492 -0
  168. package/.next/standalone/fp-cloud-cli/fp_cli/auth.py +160 -0
  169. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +1694 -0
  170. package/.next/standalone/fp-cloud-cli/fp_cli/commands/__init__.py +0 -0
  171. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +214 -0
  172. package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +407 -0
  173. package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +445 -0
  174. package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +1054 -0
  175. package/.next/standalone/fp-cloud-cli/fp_cli/commands/auth_cmds.py +512 -0
  176. package/.next/standalone/fp-cloud-cli/fp_cli/commands/errors_cmds.py +190 -0
  177. package/.next/standalone/fp-cloud-cli/fp_cli/commands/evals_cmds.py +161 -0
  178. package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +159 -0
  179. package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +416 -0
  180. package/.next/standalone/fp-cloud-cli/fp_cli/commands/guardrails_cmds.py +148 -0
  181. package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +472 -0
  182. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +407 -0
  183. package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +63 -0
  184. package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +319 -0
  185. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +499 -0
  186. package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +378 -0
  187. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +151 -0
  188. package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +150 -0
  189. package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +35 -0
  190. package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +404 -0
  191. package/.next/standalone/fp-cloud-cli/fp_cli/config.py +330 -0
  192. package/.next/standalone/fp-cloud-cli/fp_cli/dates.py +78 -0
  193. package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +345 -0
  194. package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +98 -0
  195. package/.next/standalone/fp-cloud-cli/fp_cli/models.py +891 -0
  196. package/.next/standalone/fp-cloud-cli/fp_cli/orgs.py +30 -0
  197. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +6593 -0
  198. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +208 -0
  199. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +290 -0
  200. package/.next/standalone/fp-cloud-cli/fp_cli/py.typed +0 -0
  201. package/.next/standalone/fp-cloud-cli/fp_cli/select.py +322 -0
  202. package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +53 -0
  203. package/.next/standalone/fp-cloud-cli/pyproject.toml +106 -0
  204. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +302 -0
  205. package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +8 -0
  206. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +254 -0
  207. package/.next/standalone/fp-cloud-cli/tests/__init__.py +0 -0
  208. package/.next/standalone/fp-cloud-cli/tests/conftest.py +92 -0
  209. package/.next/standalone/fp-cloud-cli/tests/test_alerting.py +644 -0
  210. package/.next/standalone/fp-cloud-cli/tests/test_analytics.py +385 -0
  211. package/.next/standalone/fp-cloud-cli/tests/test_audits.py +950 -0
  212. package/.next/standalone/fp-cloud-cli/tests/test_auth.py +98 -0
  213. package/.next/standalone/fp-cloud-cli/tests/test_auth_mode.py +272 -0
  214. package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +118 -0
  215. package/.next/standalone/fp-cloud-cli/tests/test_client.py +299 -0
  216. package/.next/standalone/fp-cloud-cli/tests/test_commands.py +814 -0
  217. package/.next/standalone/fp-cloud-cli/tests/test_config.py +99 -0
  218. package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +340 -0
  219. package/.next/standalone/fp-cloud-cli/tests/test_dates.py +76 -0
  220. package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +481 -0
  221. package/.next/standalone/fp-cloud-cli/tests/test_facets.py +170 -0
  222. package/.next/standalone/fp-cloud-cli/tests/test_failproofai_home.py +722 -0
  223. package/.next/standalone/fp-cloud-cli/tests/test_fp_home_contract.py +198 -0
  224. package/.next/standalone/fp-cloud-cli/tests/test_hardening.py +186 -0
  225. package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +127 -0
  226. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +496 -0
  227. package/.next/standalone/fp-cloud-cli/tests/test_list.py +59 -0
  228. package/.next/standalone/fp-cloud-cli/tests/test_multivalue.py +183 -0
  229. package/.next/standalone/fp-cloud-cli/tests/test_no_customer_identifiers.py +168 -0
  230. package/.next/standalone/fp-cloud-cli/tests/test_operator.py +492 -0
  231. package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +692 -0
  232. package/.next/standalone/fp-cloud-cli/tests/test_output.py +1732 -0
  233. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +191 -0
  234. package/.next/standalone/fp-cloud-cli/tests/test_readme_matches_reality.py +125 -0
  235. package/.next/standalone/fp-cloud-cli/tests/test_review_fixes.py +161 -0
  236. package/.next/standalone/fp-cloud-cli/tests/test_telemetry_completeness.py +90 -0
  237. package/.next/standalone/fp-cloud-cli/tests/test_usage.py +115 -0
  238. package/.next/standalone/fp-cloud-cli/tests/test_v1_origin_diagnostic.py +70 -0
  239. package/.next/standalone/fp-cloud-cli/tests/test_v1_routing.py +214 -0
  240. package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +104 -0
  241. package/.next/standalone/fp-cloud-cli/uv.lock +552 -0
  242. package/.next/standalone/package.json +27 -21
  243. package/.next/standalone/sdk/python/CHANGELOG.md +70 -0
  244. package/.next/standalone/sdk/python/LICENSE +42 -0
  245. package/.next/standalone/sdk/python/MANIFEST.in +21 -0
  246. package/.next/standalone/sdk/python/README.md +466 -0
  247. package/.next/standalone/sdk/python/docs/README.md +249 -0
  248. package/.next/standalone/sdk/python/docs/_shared/README.md +43 -0
  249. package/.next/standalone/sdk/python/docs/_shared/__init__.py +195 -0
  250. package/.next/standalone/sdk/python/docs/crewai/README.md +328 -0
  251. package/.next/standalone/sdk/python/docs/crewai/examples/quickstart.py +61 -0
  252. package/.next/standalone/sdk/python/docs/crewai/examples/research_crew.py +92 -0
  253. package/.next/standalone/sdk/python/docs/langgraph/README.md +481 -0
  254. package/.next/standalone/sdk/python/docs/langgraph/examples/quickstart.py +51 -0
  255. package/.next/standalone/sdk/python/docs/langgraph/examples/research_agent.py +86 -0
  256. package/.next/standalone/sdk/python/docs/langgraph/examples/supervisor_handoff.py +83 -0
  257. package/.next/standalone/sdk/python/docs/llama_index/README.md +332 -0
  258. package/.next/standalone/sdk/python/docs/llama_index/examples/quickstart.py +62 -0
  259. package/.next/standalone/sdk/python/docs/llama_index/examples/research_agent.py +93 -0
  260. package/.next/standalone/sdk/python/docs/manual/README.md +491 -0
  261. package/.next/standalone/sdk/python/docs/manual/examples/quickstart.py +57 -0
  262. package/.next/standalone/sdk/python/docs/manual/examples/research_agent.py +133 -0
  263. package/.next/standalone/sdk/python/docs/pydantic_ai/README.md +323 -0
  264. package/.next/standalone/sdk/python/docs/pydantic_ai/examples/quickstart.py +48 -0
  265. package/.next/standalone/sdk/python/docs/pydantic_ai/examples/research_agent.py +94 -0
  266. package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +131 -0
  267. package/.next/standalone/sdk/python/failproofai_sdk/_context.py +218 -0
  268. package/.next/standalone/sdk/python/failproofai_sdk/_environment.py +78 -0
  269. package/.next/standalone/sdk/python/failproofai_sdk/_events.py +752 -0
  270. package/.next/standalone/sdk/python/failproofai_sdk/_resolver.py +118 -0
  271. package/.next/standalone/sdk/python/failproofai_sdk/_runtime.py +21 -0
  272. package/.next/standalone/sdk/python/failproofai_sdk/_schema.py +317 -0
  273. package/.next/standalone/sdk/python/failproofai_sdk/_scopes.py +422 -0
  274. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -0
  275. package/.next/standalone/sdk/python/failproofai_sdk/_writer.py +750 -0
  276. package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +274 -0
  277. package/.next/standalone/sdk/python/failproofai_sdk/integrations/_compat.py +247 -0
  278. package/.next/standalone/sdk/python/failproofai_sdk/integrations/_core.py +1076 -0
  279. package/.next/standalone/sdk/python/failproofai_sdk/integrations/crewai.py +1669 -0
  280. package/.next/standalone/sdk/python/failproofai_sdk/integrations/langchain.py +2151 -0
  281. package/.next/standalone/sdk/python/failproofai_sdk/integrations/llama_index.py +1588 -0
  282. package/.next/standalone/sdk/python/failproofai_sdk/integrations/pydantic_ai.py +977 -0
  283. package/.next/standalone/sdk/python/failproofai_sdk/py.typed +0 -0
  284. package/.next/standalone/sdk/python/pyproject.toml +92 -0
  285. package/.next/standalone/sdk/python/skill/SKILL.md +399 -0
  286. package/.next/standalone/sdk/python/skill/agents/openai.yaml +8 -0
  287. package/.next/standalone/sdk/python/skill/references/events.md +234 -0
  288. package/.next/standalone/sdk/python/skill/references/frameworks.md +314 -0
  289. package/.next/standalone/sdk/python/skill/references/install.md +100 -0
  290. package/.next/standalone/sdk/python/skill/references/integration.md +352 -0
  291. package/.next/standalone/sdk/python/tests/__init__.py +0 -0
  292. package/.next/standalone/sdk/python/tests/conftest.py +169 -0
  293. package/.next/standalone/sdk/python/tests/integrations/__init__.py +6 -0
  294. package/.next/standalone/sdk/python/tests/integrations/test_crewai.py +1299 -0
  295. package/.next/standalone/sdk/python/tests/integrations/test_langchain.py +2338 -0
  296. package/.next/standalone/sdk/python/tests/integrations/test_llama_index.py +1576 -0
  297. package/.next/standalone/sdk/python/tests/integrations/test_pydantic_ai.py +1102 -0
  298. package/.next/standalone/sdk/python/tests/test_context.py +410 -0
  299. package/.next/standalone/sdk/python/tests/test_docs.py +411 -0
  300. package/.next/standalone/sdk/python/tests/test_durability.py +1352 -0
  301. package/.next/standalone/sdk/python/tests/test_encoding.py +571 -0
  302. package/.next/standalone/sdk/python/tests/test_integrations.py +1127 -0
  303. package/.next/standalone/sdk/python/tests/test_no_customer_identifiers.py +214 -0
  304. package/.next/standalone/sdk/python/tests/test_packaging.py +185 -0
  305. package/.next/standalone/sdk/python/tests/test_resolver_umbrella.py +178 -0
  306. package/.next/standalone/sdk/python/tests/test_scopes.py +487 -0
  307. package/.next/standalone/sdk/python/tests/test_sdk.py +923 -0
  308. package/.next/standalone/sdk/python/tests/test_server_contract.py +650 -0
  309. package/.next/standalone/sdk/python/tests/test_site_docs.py +371 -0
  310. package/.next/standalone/sdk/python/tests/test_skill_snippets.py +165 -0
  311. package/.next/standalone/sdk/python/tests/test_spool_contract.py +287 -0
  312. package/.next/standalone/sdk/python/tests/test_spool_creation.py +360 -0
  313. package/.next/standalone/sdk/python/tests/test_wire_format.py +279 -0
  314. package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +378 -0
  315. package/.next/standalone/sdk/python/uv.lock +6005 -0
  316. package/.next/standalone/server.js +1 -1
  317. package/README.md +77 -21
  318. package/bin/failproofai.mjs +579 -306
  319. package/dist/cli.mjs +6499 -3662
  320. package/dist/index.js +5 -5
  321. package/dist/worker.mjs +2390 -1978
  322. package/package.json +27 -21
  323. package/scripts/build-policy-pack.mjs +121 -0
  324. package/scripts/changelog-section.py +77 -0
  325. package/scripts/prune-standalone.mjs +3 -0
  326. package/scripts/python-version.py +232 -0
  327. package/scripts/translate-docs/cli.ts +21 -0
  328. package/scripts/translate-docs/mintlify-nav.ts +85 -13
  329. package/scripts/translate-docs/translator.ts +66 -11
  330. package/src/audit/cache.ts +40 -3
  331. package/src/audit/cli.ts +1 -1
  332. package/src/audit/index.ts +1 -1
  333. package/src/audit/replay.ts +89 -3
  334. package/src/audit/report.ts +5 -5
  335. package/src/audit/schedule-cli.ts +49 -53
  336. package/src/hooks/builtin-policies.ts +217 -579
  337. package/src/hooks/cloud-enrollment-cli.ts +66 -27
  338. package/src/hooks/cloud-managed-policies.ts +6 -1
  339. package/src/hooks/configure-wizard.ts +56 -369
  340. package/src/hooks/custom-hooks-loader.ts +89 -6
  341. package/src/hooks/fp-home.ts +92 -4
  342. package/src/hooks/fp-reset.ts +34 -2
  343. package/src/hooks/handler.ts +181 -11
  344. package/src/hooks/harness-cli.ts +79 -25
  345. package/src/hooks/hook-activity-store.ts +13 -3
  346. package/src/hooks/install-prompt.ts +25 -3
  347. package/src/hooks/manager.ts +424 -172
  348. package/src/hooks/migrations.ts +2 -2
  349. package/src/hooks/pack-cli.ts +885 -0
  350. package/src/hooks/pack-failclosed.ts +189 -0
  351. package/src/hooks/pack-manifest.ts +352 -0
  352. package/src/hooks/pack-store.ts +891 -0
  353. package/src/hooks/policy-catalog.ts +535 -0
  354. package/src/hooks/policy-evaluator.ts +24 -16
  355. package/src/hooks/policy-registry.ts +8 -2
  356. package/src/hooks/policy-types.ts +26 -0
  357. package/src/hooks/session-pause-cli.ts +33 -9
  358. package/src/hooks/tui.ts +695 -39
  359. package/src/hooks/types.ts +15 -1
  360. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
  361. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
  362. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +0 -3
  363. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +0 -3
  364. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
  365. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
  366. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +0 -3
  367. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
  368. package/.next/standalone/.next/server/chunks/_1ger0a2._.js +0 -3
  369. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
  370. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_1iiwm5e._.js +0 -3
  371. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__07tbbf3._.js +0 -3
  372. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kc8_v5._.js +0 -4
  373. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +0 -4
  374. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mhqk5a._.js +0 -4
  375. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pltpn8._.js +0 -4
  376. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qkcyf-._.js +0 -4
  377. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w0x_ok._.js +0 -4
  378. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +0 -4
  379. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bfex62._.js +0 -5
  380. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +0 -6
  381. package/.next/standalone/.next/server/chunks/ssr/_0lvyuv4._.js +0 -23
  382. package/.next/standalone/.next/static/chunks/2_2lhuojq2xop.js +0 -1
  383. package/.next/standalone/.next/static/chunks/36fw6yk1s6x_f.js +0 -1
  384. package/.next/standalone/.next/static/chunks/3k7mxbp9hiom0.js +0 -1
  385. package/.next/standalone/.next/static/chunks/3tzq0260n17ae.js +0 -6
  386. package/.next/standalone/.next/static/chunks/3wq9x0c-7ld5d.css +0 -2
  387. package/src/hooks/policy-presets.ts +0 -135
  388. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → I5kEb9kSgYS63j4iqfVLO}/_buildManifest.js +0 -0
  389. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → I5kEb9kSgYS63j4iqfVLO}/_clientMiddlewareManifest.js +0 -0
  390. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → I5kEb9kSgYS63j4iqfVLO}/_ssgManifest.js +0 -0
package/src/hooks/tui.ts CHANGED
@@ -14,8 +14,8 @@
14
14
  * repaint) and, on resolve, collapses that region to a one-line summary that
15
15
  * stays on screen — so the next prompt simply appends below, building the log.
16
16
  * No external dependencies. Honors NO_COLOR and non-TTY (returns the default
17
- * without drawing), and uses 24-bit color where COLORTERM advertises it,
18
- * falling back to the nearest basic ANSI hue otherwise.
17
+ * without drawing), and paints at the deepest tier the terminal admits to —
18
+ * 24-bit, the xterm-256 cube, or the 16 basic ANSI hues.
19
19
  */
20
20
  import * as readline from "node:readline";
21
21
 
@@ -116,13 +116,32 @@ const FLOWER = "❋";
116
116
  // re-deriving their own copies.
117
117
  interface Hue {
118
118
  rgb: [number, number, number];
119
+ /**
120
+ * xterm-256 index for the middle tier, verified as the nearest cube entry by
121
+ * Euclidean distance rather than eyeballed.
122
+ *
123
+ * Optional because `dim` has no honest answer: its `basic` is SGR 2, an
124
+ * ATTRIBUTE that steps down whatever foreground the user's theme is already
125
+ * using, and the cube's nearest grey (243, #767676) is a fixed colour that
126
+ * fights every theme that is not ours. So dim keeps the attribute at all
127
+ * three tiers and this stays undefined.
128
+ */
129
+ c256?: number;
119
130
  basic: string;
120
131
  }
132
+ /**
133
+ * TWO accent hues and no third — the brand system states that as a rule, and
134
+ * this table used to break it: `pink` was #ff2e88, a hot pink that appears in
135
+ * no brand token, sitting beside `logoPink` #e4587d which was the real one. The
136
+ * mark and the prompts were therefore two different pinks, and neither surface
137
+ * could be recoloured without the other drifting. There is now one pink.
138
+ * Everything past the two accents is a state — amber for warn, an attribute for
139
+ * dim — never identity.
140
+ */
121
141
  const HUES = {
122
- guide: { rgb: [102, 209, 181], basic: "36" }, // teal — the policy flower / step spine
123
- pink: { rgb: [255, 46, 136], basic: "95" }, // hot pink — selection, enabled, the brand
124
- logoPink: { rgb: [228, 88, 124], basic: "95" }, // the softer artwork pink of the logomark
125
- warn: { rgb: [227, 179, 65], basic: "33" },
142
+ guide: { rgb: [102, 209, 181], c256: 79, basic: "36" }, // #66d1b5 mint — the policy flower / step spine
143
+ pink: { rgb: [228, 88, 125], c256: 168, basic: "95" }, // #e4587d — selection, enabled, the mark, the brand
144
+ warn: { rgb: [227, 179, 65], c256: 179, basic: "33" },
126
145
  dim: { rgb: [107, 118, 132], basic: "2" },
127
146
  } satisfies Record<string, Hue>;
128
147
 
@@ -137,32 +156,68 @@ export function colorsEnabled(out: TTYOut): boolean {
137
156
  * blue that appear nowhere in the brand — so `audit` looked like a different
138
157
  * product from `config`. Routing it here
139
158
  * keeps HUES the single source of truth: change a hue once and every surface
140
- * follows. Emits truecolor where the terminal advertises it, basic ANSI
141
- * otherwise; the caller still decides *whether* to colour at all.
159
+ * follows. Paints at the deepest tier the terminal admits to; the caller still
160
+ * decides *whether* to colour at all.
142
161
  */
143
162
  export function brandAnsi(role: keyof typeof HUES): string {
144
- const h = HUES[role];
145
- const code = truecolorEnabled() ? `38;2;${h.rgb.join(";")}` : h.basic;
146
- return `${ESC}[${code}m`;
163
+ return `${ESC}[${fg(HUES[role], colorTier())}m`;
147
164
  }
148
165
 
149
166
  export const ANSI_RESET = `${ESC}[0m`;
150
167
  export const ANSI_BOLD = `${ESC}[1m`;
151
168
  export const ANSI_DIM = `${ESC}[2m`;
152
- function truecolorEnabled(): boolean {
153
- return /truecolor|24bit/i.test(process.env.COLORTERM || "");
169
+
170
+ /** How much colour this terminal will actually render. */
171
+ type ColorTier = "truecolor" | "ansi256" | "basic";
172
+
173
+ /**
174
+ * Three tiers, because the two-way gate skipped the one most terminals are in.
175
+ *
176
+ * COLORTERM-or-nothing sent every terminal that renders 256 colours but does
177
+ * not advertise 24-bit — tmux and screen, ssh into a stock xterm, most CI
178
+ * runners — all the way down to the 16 basic hues, where the brand pink lands
179
+ * on generic bright magenta and the mint on generic cyan. TERM naming its own
180
+ * depth is the signal those terminals do set, and it costs one regex.
181
+ *
182
+ * Deliberately no `FORCE_COLOR` / `-256color`-less allowlist: over-claiming a
183
+ * depth prints raw escape bytes into the user's scrollback, which is worse than
184
+ * an approximate hue. Under-claiming only costs fidelity.
185
+ */
186
+ function colorTier(): ColorTier {
187
+ if (/truecolor|24bit/i.test(process.env.COLORTERM || "")) return "truecolor";
188
+ if (/256color/i.test(process.env.TERM || "")) return "ansi256";
189
+ return "basic";
154
190
  }
155
191
 
156
- /** Brand painter: role-named color functions, truecolor where advertised,
157
- * basic-ANSI fallback otherwise, identity when `on` is false. */
192
+ /** SGR foreground parameters for a hue at a tier. The ONE place a hue turns
193
+ * into bytes, so a new tier is added here and nowhere else. */
194
+ function fg(h: Hue, tier: ColorTier): string {
195
+ if (tier === "truecolor") return `38;2;${h.rgb[0]};${h.rgb[1]};${h.rgb[2]}`;
196
+ if (tier === "ansi256" && h.c256 !== undefined) return `38;5;${h.c256}`;
197
+ return h.basic;
198
+ }
199
+
200
+ /** The same as a background — only the logomark needs one, for a half-block
201
+ * cell whose two pixels are different hues. The current grid has no such cell
202
+ * (the flower and the cross never share a column), so this is here for the
203
+ * next grid edit, which the rules above LOGO_GRID actively invite. Never
204
+ * called at the basic tier: the mark draws monochrome there rather than
205
+ * approximate two brand hues with ANSI 5 and 6. */
206
+ function bg(h: Hue, tier: ColorTier): string {
207
+ return tier === "ansi256" && h.c256 !== undefined
208
+ ? `48;5;${h.c256}`
209
+ : `48;2;${h.rgb[0]};${h.rgb[1]};${h.rgb[2]}`;
210
+ }
211
+
212
+ /** Brand painter: role-named color functions at the terminal's best tier,
213
+ * identity when `on` is false. */
158
214
  export function paint(on: boolean) {
159
- const tc = on && truecolorEnabled();
215
+ const tier: ColorTier = on ? colorTier() : "basic";
160
216
  const mk =
161
217
  (h: Hue, bold = false) =>
162
218
  (s: string): string => {
163
219
  if (!on) return s;
164
- const code = tc ? `38;2;${h.rgb[0]};${h.rgb[1]};${h.rgb[2]}` : h.basic;
165
- return `${ESC}[${bold ? "1;" : ""}${code}m${s}${ESC}[0m`;
220
+ return `${ESC}[${bold ? "1;" : ""}${fg(h, tier)}m${s}${ESC}[0m`;
166
221
  };
167
222
  return {
168
223
  bold: (s: string) => (on ? `${ESC}[1m${s}${ESC}[0m` : s),
@@ -170,7 +225,11 @@ export function paint(on: boolean) {
170
225
  guide: mk(HUES.guide),
171
226
  pink: mk(HUES.pink),
172
227
  pinkBold: mk(HUES.pink, true),
173
- softPink: mk(HUES.logoPink),
228
+ // An ALIAS of `pink`, not a hue. The logomark's softer artwork tint
229
+ // collapsed into the one brand pink, so this name survives only for its
230
+ // single caller — install-prompt.ts's "beta" pill — and should be renamed
231
+ // to `pink` there, at which point it is deleted.
232
+ softPink: mk(HUES.pink),
174
233
  warn: mk(HUES.warn),
175
234
  };
176
235
  }
@@ -224,37 +283,40 @@ const LOGO_GRID = [
224
283
  "..ppppppppppp",
225
284
  "..ppppppppppp",
226
285
  ];
227
- // Derived from the shared HUES table so a palette tweak needs one edit.
228
- const LOGO_TEAL: [number, number, number] = HUES.guide.rgb;
229
- const LOGO_PINK: [number, number, number] = HUES.logoPink.rgb;
230
-
231
- /** Render the logomark as half-block art. When `colorize` is false (no truecolor
232
- * / NO_COLOR) the shape still prints, just monochrome. */
233
- function renderLogo(colorize: boolean): string[] {
286
+ // Derived from the shared HUES table so a palette tweak needs one edit. The
287
+ // mark is painted from the SAME two accents as the prompts — it used to carry
288
+ // its own pink, which is how the two drifted apart.
289
+ const LOGO_TEAL: Hue = HUES.guide;
290
+ const LOGO_PINK: Hue = HUES.pink;
291
+
292
+ /** Render the logomark as half-block art. At the `basic` tier (16 colours, or
293
+ * NO_COLOR) the shape still prints, just monochrome — approximating two brand
294
+ * hues with generic magenta and cyan reads as a different mark, and the shape
295
+ * alone already carries it. */
296
+ function renderLogo(tier: ColorTier): string[] {
234
297
  const pad = ".".repeat(LOGO_GRID[0]?.length ?? 0);
235
- const rgb = (ch: string): [number, number, number] | null =>
236
- ch === "t" ? LOGO_TEAL : ch === "p" ? LOGO_PINK : null;
298
+ const hue = (ch: string): Hue | null => (ch === "t" ? LOGO_TEAL : ch === "p" ? LOGO_PINK : null);
237
299
  const lines: string[] = [];
238
300
  for (let r = 0; r < LOGO_GRID.length; r += 2) {
239
301
  const top = LOGO_GRID[r];
240
302
  const bot = LOGO_GRID[r + 1] ?? pad;
241
303
  let line = "";
242
304
  for (let x = 0; x < top.length; x++) {
243
- const t = rgb(top[x]);
244
- const b = rgb(bot[x]);
305
+ const t = hue(top[x]);
306
+ const b = hue(bot[x]);
245
307
  if (!t && !b) {
246
308
  line += " ";
247
- } else if (!colorize) {
309
+ } else if (tier === "basic") {
248
310
  line += t && b ? "█" : t ? "▀" : "▄";
249
311
  } else if (t && b) {
250
312
  line +=
251
313
  t === b
252
- ? `${ESC}[38;2;${t.join(";")}m█${ESC}[0m`
253
- : `${ESC}[38;2;${t.join(";")};48;2;${b.join(";")}m▀${ESC}[0m`;
314
+ ? `${ESC}[${fg(t, tier)}m█${ESC}[0m`
315
+ : `${ESC}[${fg(t, tier)};${bg(b, tier)}m▀${ESC}[0m`;
254
316
  } else if (t) {
255
- line += `${ESC}[38;2;${t.join(";")}m▀${ESC}[0m`;
317
+ line += `${ESC}[${fg(t, tier)}m▀${ESC}[0m`;
256
318
  } else {
257
- line += `${ESC}[38;2;${b!.join(";")}m▄${ESC}[0m`;
319
+ line += `${ESC}[${fg(b!, tier)}m▄${ESC}[0m`;
258
320
  }
259
321
  }
260
322
  lines.push(line);
@@ -320,8 +382,10 @@ export function renderBrandLogo(stdout: TTYOut = process.stdout): string[] {
320
382
  if (cols < LOGO_MIN_COLS) {
321
383
  return [`${c.guide(FLOWER)} fa${c.pink("il")}proof ai ${c.dim("· " + TAGLINE)}`];
322
384
  }
323
- const tc = colorsEnabled(stdout) && truecolorEnabled();
324
- const lines = renderLogo(tc).map((l) => ` ${l}`);
385
+ // NO_COLOR / non-TTY forces `basic`, which renderLogo draws monochrome — so
386
+ // the mark never emits an escape byte on a stream that asked for none.
387
+ const tier: ColorTier = colorsEnabled(stdout) ? colorTier() : "basic";
388
+ const lines = renderLogo(tier).map((l) => ` ${l}`);
325
389
  lines.push("");
326
390
  lines.push(` fa${c.pink("il")}proof ai`);
327
391
  lines.push(` ${c.dim(TAGLINE)}`);
@@ -357,6 +421,7 @@ export function renderLaunchBanner(version: string, stdout: TTYOut = process.std
357
421
  row("star", c.dim("https://github.com/failproofai/failproofai")),
358
422
  row("docs", c.dim("https://docs.befailproof.ai/introduction")),
359
423
  row("discord", c.dim("https://discord.befailproof.ai/")),
424
+ row("reddit", c.dim("https://www.reddit.com/r/failproofai/")),
360
425
  "",
361
426
  ];
362
427
  }
@@ -444,6 +509,24 @@ function nameWidth(labels: string[]): number {
444
509
  return Math.min(24, Math.max(6, ...labels.map((l) => l.length)));
445
510
  }
446
511
 
512
+ /**
513
+ * The hint budget for one row, after a label that outgrew its column.
514
+ *
515
+ * `nameWidth` caps the name column at 24 so that one long name cannot squeeze
516
+ * every description on screen — but `padEnd` pads, it does not TRUNCATE, so a
517
+ * longer name renders at its true width while the description was sized against
518
+ * the cap. The row then overruns the terminal by the difference and the
519
+ * description is cut by the terminal instead of by `ellipsize`, with no `…` to
520
+ * show it happened.
521
+ *
522
+ * Names are deliberately not truncated: a policy name is the thing you type
523
+ * next, and half of one is useless. The description gives up the space, because
524
+ * it is prose and shortening prose costs nothing.
525
+ */
526
+ function hintBudget(label: string, nameCol: number, budget: number): number {
527
+ return Math.max(6, budget - Math.max(0, label.length - nameCol));
528
+ }
529
+
447
530
  type DisplayRow = { kind: "header"; text: string } | { kind: "item"; index: number };
448
531
 
449
532
  /** Flatten choices into section-header + item display rows. */
@@ -650,7 +733,9 @@ export function selectOne<T>(opts: SelectOneOptions<T>): Promise<T | Back | null
650
733
  const dot = active ? c.pink(RADIO_ON) : c.dim(RADIO_OFF);
651
734
  const rawLabel = choice.label.padEnd(nameCol);
652
735
  const label = active ? c.pinkBold(rawLabel) : rawLabel;
653
- const hint = choice.hint ? ` ${c.dim(ellipsize(choice.hint, budget))}` : "";
736
+ const hint = choice.hint
737
+ ? ` ${c.dim(ellipsize(choice.hint, hintBudget(choice.label, nameCol, budget)))}`
738
+ : "";
654
739
  return `${dot} ${label}${hint}`;
655
740
  },
656
741
  allowBack: opts.allowBack,
@@ -708,7 +793,9 @@ export function multiSelect<T>(opts: MultiSelectOptions<T>): Promise<T[] | null
708
793
  : c.dim(CHECK_OFF);
709
794
  const rawLabel = choice.label.padEnd(nameCol);
710
795
  const label = active ? c.pinkBold(rawLabel) : checked[index] ? rawLabel : c.dim(rawLabel);
711
- const hint = choice.hint ? ` ${c.dim(ellipsize(choice.hint, budget))}` : "";
796
+ const hint = choice.hint
797
+ ? ` ${c.dim(ellipsize(choice.hint, hintBudget(choice.label, nameCol, budget)))}`
798
+ : "";
712
799
  return `${caret} ${box} ${label}${hint}`;
713
800
  },
714
801
  warnLine: () => (warn ? c.warn(`Select at least ${minSelected}.`) : null),
@@ -901,3 +988,572 @@ export function promptText(opts: PromptTextOptions): Promise<string | null> {
901
988
  stdin.on("keypress", onKey);
902
989
  });
903
990
  }
991
+
992
+ // ── the kit ──────────────────────────────────────────────────────────────────
993
+ /**
994
+ * The block builders every PRINTED surface is assembled from.
995
+ *
996
+ * The prompts above dress the wizard. Everything else the CLI prints grew its
997
+ * own dialect instead: `policies` renders a table with `── rules ──` and colored
998
+ * chips, `pack list` and `harness list` print a bare sentence plus an indented
999
+ * example, `config --status` opens with a prose line and then label/value rows at
1000
+ * label width 9, `audit --status` indents by three and misaligns its own value
1001
+ * column (col 21 on the first row, 18 on the rest, plus a whitespace-only line),
1002
+ * and `uninstall` prints `•` bullets with no header and no color at all. Six
1003
+ * answers to "how does this product state a fact".
1004
+ *
1005
+ * These are the one answer. Every builder is pure — `(spec, opts) => string[]` —
1006
+ * so a surface can be asserted at any width, with color on or off, without a pty;
1007
+ * that is the same shape `renderBrandLogo`, `reviewLines` and `buildSummary`
1008
+ * already have, and the reason they are the only rendering we can currently test.
1009
+ *
1010
+ * Callers pass `optsFor(stdout)` and print with `printBlock`, which owns the
1011
+ * outer margins so no surface has to remember them.
1012
+ */
1013
+
1014
+ /** Every printed line starts here. Two spaces, never three. */
1015
+ export const INDENT = " ";
1016
+
1017
+ export interface RenderOpts {
1018
+ /** Terminal width. Defaults to 80 so a piped or asserted render is deterministic. */
1019
+ cols?: number;
1020
+ /** Whether to emit ANSI at all. Defaults to OFF — colour is opt-in via `optsFor`. */
1021
+ color?: boolean;
1022
+ }
1023
+
1024
+ function ctx(opts?: RenderOpts): { cols: number; c: ReturnType<typeof paint> } {
1025
+ return { cols: Math.max(20, opts?.cols ?? 80), c: paint(opts?.color ?? false) };
1026
+ }
1027
+
1028
+ /** Derive render options from a real stream, honouring NO_COLOR and non-TTY. */
1029
+ export function optsFor(stdout: TTYOut = process.stdout): Required<RenderOpts> {
1030
+ return { cols: stdout.columns || 80, color: colorsEnabled(stdout) };
1031
+ }
1032
+
1033
+ /** Visible width of a line, skipping ANSI CSI sequences — the counterpart to
1034
+ * `truncate`, needed wherever a column has to line up under coloured content. */
1035
+ export function visibleWidth(line: string): number {
1036
+ let width = 0;
1037
+ let i = 0;
1038
+ while (i < line.length) {
1039
+ if (line[i] === ESC && line[i + 1] === "[") {
1040
+ let j = i + 2;
1041
+ while (j < line.length && !/[A-Za-z]/.test(line[j])) j++;
1042
+ i = j + 1;
1043
+ } else {
1044
+ width++;
1045
+ i++;
1046
+ }
1047
+ }
1048
+ return width;
1049
+ }
1050
+
1051
+ /** Wrap PLAIN text to `width`. A single word longer than the budget (a path, a
1052
+ * URL) overflows its own line rather than being broken — a split path is worse
1053
+ * than a long one, because it cannot be copied. */
1054
+ export function wrap(text: string, width: number): string[] {
1055
+ if (width <= 0) return [text];
1056
+ const words = text.split(/\s+/).filter(Boolean);
1057
+ if (words.length === 0) return [];
1058
+ const out: string[] = [];
1059
+ let line = "";
1060
+ for (const word of words) {
1061
+ if (!line) line = word;
1062
+ else if (line.length + 1 + word.length <= width) line += ` ${word}`;
1063
+ else {
1064
+ out.push(line);
1065
+ line = word;
1066
+ }
1067
+ }
1068
+ if (line) out.push(line);
1069
+ return out;
1070
+ }
1071
+
1072
+ /** One visible character plus whatever SGR codes are in effect at it. */
1073
+ interface AnsiCell {
1074
+ ch: string;
1075
+ active: string;
1076
+ }
1077
+
1078
+ function toAnsiCells(text: string): AnsiCell[] {
1079
+ const cells: AnsiCell[] = [];
1080
+ let active = "";
1081
+ let i = 0;
1082
+ while (i < text.length) {
1083
+ if (text[i] === ESC && text[i + 1] === "[") {
1084
+ let j = i + 2;
1085
+ while (j < text.length && !/[A-Za-z]/.test(text[j])) j++;
1086
+ const code = text.slice(i, j + 1);
1087
+ // A reset clears everything; anything else stacks on top.
1088
+ active = /\[0?m$/.test(code) ? "" : active + code;
1089
+ i = j + 1;
1090
+ } else {
1091
+ cells.push({ ch: text[i], active });
1092
+ i++;
1093
+ }
1094
+ }
1095
+ return cells;
1096
+ }
1097
+
1098
+ function renderAnsiCells(line: AnsiCell[]): string {
1099
+ let out = "";
1100
+ let active = "";
1101
+ for (const cell of line) {
1102
+ if (cell.active !== active) {
1103
+ if (active) out += ANSI_RESET;
1104
+ out += cell.active;
1105
+ active = cell.active;
1106
+ }
1107
+ out += cell.ch;
1108
+ }
1109
+ // Every produced line closes what it opened. A line that ends mid-SGR bleeds
1110
+ // its colour into everything printed after it.
1111
+ return active ? out + ANSI_RESET : out;
1112
+ }
1113
+
1114
+ /**
1115
+ * Wrap text that carries colour, on visible width.
1116
+ *
1117
+ * `wrap` counts escape bytes as characters, so a coloured value used to be
1118
+ * handed back unwrapped — and then `writeLines` cut it at the terminal edge with
1119
+ * `truncate`, which stops at the first plain character past the limit. That lost
1120
+ * the tail of the sentence with no ellipsis to say so, AND dropped the closing
1121
+ * reset, so the colour ran on into every line that followed. Both were
1122
+ * reproduced on `audit --status` at 80 columns.
1123
+ */
1124
+ export function wrapAnsi(text: string, width: number): string[] {
1125
+ if (width <= 0) return [text];
1126
+ if (!text.includes(ESC)) return wrap(text, width);
1127
+ const words: AnsiCell[][] = [];
1128
+ let word: AnsiCell[] = [];
1129
+ for (const cell of toAnsiCells(text)) {
1130
+ if (/\s/.test(cell.ch)) {
1131
+ if (word.length > 0) words.push(word);
1132
+ word = [];
1133
+ } else {
1134
+ word.push(cell);
1135
+ }
1136
+ }
1137
+ if (word.length > 0) words.push(word);
1138
+
1139
+ const lines: string[] = [];
1140
+ let line: AnsiCell[] = [];
1141
+ for (const next of words) {
1142
+ if (line.length > 0 && line.length + 1 + next.length > width) {
1143
+ lines.push(renderAnsiCells(line));
1144
+ line = [...next];
1145
+ continue;
1146
+ }
1147
+ if (line.length > 0) line.push({ ch: " ", active: line[line.length - 1].active });
1148
+ line.push(...next);
1149
+ }
1150
+ if (line.length > 0) lines.push(renderAnsiCells(line));
1151
+ return lines;
1152
+ }
1153
+
1154
+ /** Close any SGR a hard cut left open, for the same reason. */
1155
+ function closeSgr(text: string): string {
1156
+ const cells = toAnsiCells(text);
1157
+ const last = cells[cells.length - 1];
1158
+ return last && last.active ? text + ANSI_RESET : text;
1159
+ }
1160
+
1161
+ /** Fit one cell to `width`: ANSI-safe hard cut when it carries colour, a proper
1162
+ * single-ellipsis cut when it is plain. */
1163
+ function fit(cell: string, width: number): string {
1164
+ if (visibleWidth(cell) <= width) return cell;
1165
+ return cell.includes(ESC) ? closeSgr(truncate(cell, width)) : ellipsize(cell, width);
1166
+ }
1167
+
1168
+ /** Pad a possibly-coloured cell to `width` visible columns. */
1169
+ function pad(cell: string, width: number, align: "left" | "right" = "left"): string {
1170
+ const gap = Math.max(0, width - visibleWidth(cell));
1171
+ return align === "right" ? " ".repeat(gap) + cell : cell + " ".repeat(gap);
1172
+ }
1173
+
1174
+ /**
1175
+ * Join blocks with exactly one blank line between them.
1176
+ *
1177
+ * This is blank-line discipline as code rather than as a rule people remember:
1178
+ * whitespace-only lines normalise to empty, two blanks never survive next to
1179
+ * each other, and a block cannot open with one. `audit --status` prints a line
1180
+ * containing a single space today; assembled through here it cannot.
1181
+ */
1182
+ export function stack(...groups: Array<string[] | null | undefined>): string[] {
1183
+ const out: string[] = [];
1184
+ for (const group of groups) {
1185
+ if (!group || group.length === 0) continue;
1186
+ const body: string[] = [];
1187
+ for (const line of group) {
1188
+ const normalised = line.trim() === "" ? "" : line;
1189
+ if (normalised === "" && (body.length === 0 || body[body.length - 1] === "")) continue;
1190
+ body.push(normalised);
1191
+ }
1192
+ while (body.length > 0 && body[body.length - 1] === "") body.pop();
1193
+ if (body.length === 0) continue;
1194
+ if (out.length > 0) out.push("");
1195
+ out.push(...body);
1196
+ }
1197
+ return out;
1198
+ }
1199
+
1200
+ /**
1201
+ * Print an assembled block with its outer margins. One place decides them.
1202
+ *
1203
+ * Deliberately NOT `writeLines`, which truncates each line to the terminal
1204
+ * width. Every builder above already fits its content, so the only lines that
1205
+ * can exceed the width are ones holding a token that cannot be broken — a path,
1206
+ * a URL, a session id. Cutting those loses characters silently and with no
1207
+ * ellipsis to admit it; letting the terminal wrap keeps them copyable, which is
1208
+ * the entire reason they are printed.
1209
+ */
1210
+ export function printBlock(stdout: TTYOut, lines: string[]): void {
1211
+ if (lines.length === 0) return;
1212
+ stdout.write(["", ...lines.map(closeSgr), ""].join("\n") + "\n");
1213
+ }
1214
+
1215
+ /**
1216
+ * The heading every surface opens with: what you are looking at, and the state
1217
+ * it describes, right-aligned and dim.
1218
+ *
1219
+ * Six of our printed surfaces open with nothing at all, so output arrives with
1220
+ * no statement of what it is — which is survivable on a screen you asked for and
1221
+ * confusing in scrollback next to five other commands.
1222
+ */
1223
+ export function title(name: string, meta?: string, opts?: RenderOpts): string[] {
1224
+ const { cols, c } = ctx(opts);
1225
+ const left = `${INDENT}${c.bold(name)}`;
1226
+ if (!meta) return [left];
1227
+ const right = c.dim(meta);
1228
+ const gap = cols - visibleWidth(left) - visibleWidth(right) - INDENT.length;
1229
+ // Too narrow to sit on one line: drop it under rather than let it wrap into
1230
+ // the middle of the heading, where it reads as a second, broken title.
1231
+ if (gap < 2) return [left, `${INDENT}${c.dim(meta)}`];
1232
+ return [left + " ".repeat(gap) + right];
1233
+ }
1234
+
1235
+ /** A section divider — the `── Convention Policies ──────` shape `policies`
1236
+ * already uses, available to every surface instead of one. */
1237
+ export function rule(label?: string, opts?: RenderOpts): string[] {
1238
+ const { cols, c } = ctx(opts);
1239
+ const width = Math.max(4, cols - INDENT.length * 2);
1240
+ if (!label) return [`${INDENT}${c.dim("─".repeat(width))}`];
1241
+ const head = `── ${label} `;
1242
+ const tail = Math.max(3, width - head.length);
1243
+ return [`${INDENT}${c.dim(head + "─".repeat(tail))}`];
1244
+ }
1245
+
1246
+ export interface Row {
1247
+ label: string;
1248
+ value: string;
1249
+ }
1250
+
1251
+ /**
1252
+ * Label/value rows on ONE computed column.
1253
+ *
1254
+ * The column is derived from the widest label in the block and never hardcoded,
1255
+ * which is the entire fix for `audit --status` printing its first row's value at
1256
+ * column 21 and the rest at 18 — two hand-counted paddings in one block, in the
1257
+ * same file.
1258
+ */
1259
+ export function rows(items: Array<Row | [string, string]>, opts?: RenderOpts): string[] {
1260
+ const { cols, c } = ctx(opts);
1261
+ const pairs = items.map((item) =>
1262
+ Array.isArray(item) ? { label: item[0], value: item[1] } : item,
1263
+ );
1264
+ if (pairs.length === 0) return [];
1265
+ // The column grows to the widest label and the label is NEVER cut. It used to
1266
+ // be capped at 24 and ellipsized, which quietly ate the pause session id —
1267
+ // the one string `--resume --session <id>` needs, printed nowhere else. Half
1268
+ // an id is not a shorter fact, it is an unusable one; a label past the cap
1269
+ // takes its own line instead, so the value column survives.
1270
+ const widest = Math.max(...pairs.map((p) => visibleWidth(p.label)));
1271
+ const labelWidth = Math.min(widest, Math.max(12, Math.floor((cols - INDENT.length * 2) / 2)));
1272
+ const valueBudget = Math.max(8, cols - INDENT.length * 2 - labelWidth - 2);
1273
+ const out: string[] = [];
1274
+ for (const { label, value } of pairs) {
1275
+ if (visibleWidth(label) > labelWidth) {
1276
+ out.push(`${INDENT}${c.dim(label)}`);
1277
+ for (const extra of wrapAnsi(value, valueBudget)) {
1278
+ out.push(" ".repeat(INDENT.length + labelWidth + 2) + extra);
1279
+ }
1280
+ continue;
1281
+ }
1282
+ const gutter = `${INDENT}${pad(c.dim(label), labelWidth)} `;
1283
+ const hang = " ".repeat(visibleWidth(gutter));
1284
+ // Wrapped with a hanging indent rather than cut. Cutting looks tidier and is
1285
+ // worse: these values are URLs, machine ids and paths, and half of one is
1286
+ // not a shorter fact, it is an unusable one. Neither `wrap` nor `wrapAnsi`
1287
+ // splits a single token, so a long URL survives whole on its own line — the
1288
+ // only case that can still pass the right edge, and the right trade.
1289
+ const wrapped = wrapAnsi(value, valueBudget);
1290
+ if (wrapped.length === 0) {
1291
+ out.push(gutter.trimEnd());
1292
+ continue;
1293
+ }
1294
+ out.push(gutter + wrapped[0]);
1295
+ for (const extra of wrapped.slice(1)) out.push(hang + extra);
1296
+ }
1297
+ return out;
1298
+ }
1299
+
1300
+ /** A row, optionally with dim continuation lines under it — configured params,
1301
+ * a policy hint, the reason a file would not load. */
1302
+ export interface TableRow {
1303
+ cells: string[];
1304
+ notes?: string[];
1305
+ }
1306
+
1307
+ /** A heading between rows. Widths are computed across the WHOLE table, so the
1308
+ * columns line up through every section instead of jumping at each one — which
1309
+ * is what a table-per-section does, along with repeating the column labels. */
1310
+ export interface TableSection {
1311
+ section: string;
1312
+ }
1313
+
1314
+ export interface TableSpec {
1315
+ head: string[];
1316
+ rows: Array<string[] | TableRow | TableSection>;
1317
+ /** Per-column alignment. Numbers read right, everything else left. */
1318
+ align?: Array<"left" | "right">;
1319
+ /** Which column absorbs the leftover width (default: the last). */
1320
+ flex?: number;
1321
+ /**
1322
+ * Columns the shrink pass may not touch. The second pass takes width from the
1323
+ * WIDEST column, which in a path table is the path — cutting the one value the
1324
+ * listing exists to hand back to the user.
1325
+ */
1326
+ protect?: number[];
1327
+ }
1328
+
1329
+ /** The `policies` table, available to every surface that lists things. */
1330
+ export function table(spec: TableSpec, opts?: RenderOpts): string[] {
1331
+ const { cols, c } = ctx(opts);
1332
+ const count = spec.head.length;
1333
+ if (count === 0) return [];
1334
+ const flex = spec.flex ?? count - 1;
1335
+ const body: Array<TableRow | TableSection> = spec.rows.map((r) =>
1336
+ Array.isArray(r) ? { cells: r } : r,
1337
+ );
1338
+ const dataRows = body.filter((r): r is TableRow => "cells" in r);
1339
+ const natural = spec.head.map((h, i) =>
1340
+ Math.max(visibleWidth(h), ...dataRows.map((r) => visibleWidth(r.cells[i] ?? ""))),
1341
+ );
1342
+ const budget = cols - INDENT.length * 2 - (count - 1) * 2;
1343
+ // The flex column gives way first, and only when it has nothing left to give
1344
+ // do the others shrink — widest first, so a narrow terminal costs the column
1345
+ // that can most afford it. Without the second pass a single long cell (a path,
1346
+ // a description) pushed the whole row past the terminal edge, which is the
1347
+ // overrun this kit exists to end.
1348
+ let overflow = natural.reduce((a, b) => a + b, 0) - budget;
1349
+ if (overflow > 0) {
1350
+ const give = Math.min(overflow, Math.max(0, natural[flex] - 8));
1351
+ natural[flex] -= give;
1352
+ overflow -= give;
1353
+ }
1354
+ const protectedCols = new Set(spec.protect ?? []);
1355
+ while (overflow > 0) {
1356
+ let widest = -1;
1357
+ for (let i = 0; i < natural.length; i += 1) {
1358
+ if (protectedCols.has(i) || natural[i] <= 4) continue;
1359
+ if (widest === -1 || natural[i] > natural[widest]) widest = i;
1360
+ }
1361
+ // Everything left is protected or already at its floor: leave the row long
1362
+ // and let the terminal wrap it rather than cut a protected value.
1363
+ if (widest === -1) break;
1364
+ natural[widest] -= 1;
1365
+ overflow -= 1;
1366
+ }
1367
+ const line = (cells: string[]) =>
1368
+ INDENT +
1369
+ cells
1370
+ .map((cell, i) => pad(fit(cell, natural[i]), natural[i], spec.align?.[i] ?? "left"))
1371
+ .join(" ")
1372
+ .trimEnd();
1373
+ // Notes hang under the row's LAST fixed column — i.e. where the name starts —
1374
+ // so a hint reads as belonging to its row rather than to the table.
1375
+ const noteIndent =
1376
+ INDENT.length + natural.slice(0, Math.max(0, flex - 1)).reduce((a, b) => a + b + 2, 0);
1377
+ // All-empty headings mean a table that does not want a header row: repeating
1378
+ // column labels above every section is chrome, not content.
1379
+ const wantsHead = spec.head.some((h) => h.length > 0);
1380
+ const out = wantsHead ? [line(spec.head.map((h) => c.dim(h))), ...rule(undefined, opts)] : [];
1381
+ for (const row of body) {
1382
+ if ("section" in row) {
1383
+ if (out.length > 0) out.push("");
1384
+ out.push(...rule(row.section, opts));
1385
+ continue;
1386
+ }
1387
+ out.push(line(row.cells));
1388
+ for (const n of row.notes ?? []) {
1389
+ for (const wrapped of wrap(n, Math.max(8, cols - noteIndent - INDENT.length))) {
1390
+ out.push(" ".repeat(noteIndent) + c.dim(wrapped));
1391
+ }
1392
+ }
1393
+ }
1394
+ return out;
1395
+ }
1396
+
1397
+ /** Every state a listed thing can be in. Symbol AND colour, never colour alone,
1398
+ * so the list still reads under NO_COLOR and for a red/green-blind reader. */
1399
+ export type ChipState =
1400
+ | "on"
1401
+ | "off"
1402
+ | "mixed"
1403
+ | "locked"
1404
+ | "cloud"
1405
+ | "pack"
1406
+ | "failed"
1407
+ | "observe";
1408
+
1409
+ const CHIP_LABELS: Record<ChipState, string> = {
1410
+ on: "✓ ON",
1411
+ off: "· OFF",
1412
+ // A convention FILE holds several hooks and some of them can be disabled
1413
+ // individually, so "on" and "off" cannot describe it between them.
1414
+ mixed: "◐ MIXED",
1415
+ locked: "✓ LOCK",
1416
+ cloud: "✓ CLOUD",
1417
+ pack: "✓ PACK",
1418
+ failed: "⚠ FAIL",
1419
+ observe: "◉ OBS",
1420
+ };
1421
+
1422
+ /** Width of the widest chip, so a column of them lines up without the caller
1423
+ * knowing which states it happens to contain. */
1424
+ export const CHIP_WIDTH = Math.max(...Object.values(CHIP_LABELS).map((l) => l.length));
1425
+
1426
+ export function chip(state: ChipState, opts?: RenderOpts): string {
1427
+ const { c } = ctx(opts);
1428
+ const label = CHIP_LABELS[state];
1429
+ const painted =
1430
+ state === "on" || state === "pack"
1431
+ ? c.pink(label)
1432
+ : state === "failed" || state === "mixed"
1433
+ ? c.warn(label)
1434
+ : state === "cloud" || state === "observe"
1435
+ ? c.guide(label)
1436
+ : c.dim(label);
1437
+ return pad(painted, CHIP_WIDTH);
1438
+ }
1439
+
1440
+ /** A bulleted list, wrapped, with continuation lines aligned under the text —
1441
+ * `uninstall` prints 200-column bullets today that wrap into column 0. */
1442
+ export function bullets(items: string[], opts?: RenderOpts): string[] {
1443
+ const { cols, c } = ctx(opts);
1444
+ const budget = Math.max(8, cols - INDENT.length - 4);
1445
+ const out: string[] = [];
1446
+ for (const item of items) {
1447
+ const [first, ...rest] = wrap(item, budget);
1448
+ if (first === undefined) continue;
1449
+ out.push(`${INDENT}${c.pink("•")} ${first}`);
1450
+ for (const line of rest) out.push(`${INDENT} ${line}`);
1451
+ }
1452
+ return out;
1453
+ }
1454
+
1455
+ /** A dim aside under a block. */
1456
+ export function note(text: string, opts?: RenderOpts): string[] {
1457
+ const { cols, c } = ctx(opts);
1458
+ return wrap(text, Math.max(8, cols - INDENT.length * 2)).map((l) => `${INDENT}${c.dim(l)}`);
1459
+ }
1460
+
1461
+ /**
1462
+ * "Here is the command to run next" — the single most repeated shape in the CLI
1463
+ * and, today, invented separately by `policies`, `pack list` and `harness list`.
1464
+ */
1465
+ export function nextStep(cmd: string, why?: string, opts?: RenderOpts): string[] {
1466
+ const { c } = ctx(opts);
1467
+ const out: string[] = [];
1468
+ if (why) out.push(...note(why, opts));
1469
+ out.push(`${INDENT}${INDENT}${c.pink(cmd)}`);
1470
+ return out;
1471
+ }
1472
+
1473
+ function gutterBlock(symbol: string, lines: string[], opts?: RenderOpts): string[] {
1474
+ const { cols } = ctx(opts);
1475
+ const budget = Math.max(8, cols - INDENT.length - 3);
1476
+ const out: string[] = [];
1477
+ for (const line of lines) {
1478
+ for (const wrapped of wrap(line, budget)) {
1479
+ out.push(out.length === 0 ? `${INDENT}${symbol} ${wrapped}` : `${INDENT} ${wrapped}`);
1480
+ }
1481
+ }
1482
+ return out;
1483
+ }
1484
+
1485
+ /** Amber gutter. One shape for every warning, whether it is two lines about
1486
+ * scopes or six about the daemon. */
1487
+ export function warning(lines: string[], opts?: RenderOpts): string[] {
1488
+ const { c } = ctx(opts);
1489
+ return gutterBlock(c.warn("⚠"), lines, opts);
1490
+ }
1491
+
1492
+ /** The same, for the ones that destroy something. */
1493
+ export function danger(lines: string[], opts?: RenderOpts): string[] {
1494
+ const { c } = ctx(opts);
1495
+ return gutterBlock(c.pink("!"), lines, opts);
1496
+ }
1497
+
1498
+ /** Nothing to show, said the same way everywhere: what is empty, then the one
1499
+ * command that changes that. */
1500
+ export function emptyState(
1501
+ spec: { what: string; hint?: string; cmd?: string },
1502
+ opts?: RenderOpts,
1503
+ ): string[] {
1504
+ return stack(note(spec.what, opts), spec.cmd ? nextStep(spec.cmd, spec.hint, opts) : null);
1505
+ }
1506
+
1507
+ export interface HelpSpec {
1508
+ usage: Array<[string, string?]>;
1509
+ options?: Array<[string, string]>;
1510
+ examples?: string[];
1511
+ /** Free lines under the heading, before USAGE. */
1512
+ lead?: string[];
1513
+ }
1514
+
1515
+ /**
1516
+ * One shape for all five help screens.
1517
+ *
1518
+ * `policy --help`, `audit --help`, `pack --help`, `harness --help` and the top
1519
+ * level each position their description column differently and indent
1520
+ * differently, so reading two of them in a row feels like two products. The
1521
+ * column is computed here from the widest entry, capped so a long flag cannot
1522
+ * push every description off the right edge.
1523
+ */
1524
+ export function helpBlock(spec: HelpSpec, opts?: RenderOpts): string[] {
1525
+ const { cols, c } = ctx(opts);
1526
+ const entries = [...spec.usage.map(([n]) => n), ...(spec.options ?? []).map(([n]) => n)];
1527
+ const nameWidth = Math.min(34, Math.max(12, ...entries.map((n) => n.length)));
1528
+ const section = (heading: string, body: string[]): string[] =>
1529
+ body.length === 0 ? [] : [c.dim(heading), ...body];
1530
+ const entryLines = (items: Array<[string, string?]>): string[] => {
1531
+ const out: string[] = [];
1532
+ for (const [name, description] of items) {
1533
+ if (!description) {
1534
+ out.push(`${INDENT}${name}`);
1535
+ continue;
1536
+ }
1537
+ const budget = Math.max(12, cols - INDENT.length * 2 - nameWidth - 2);
1538
+ const [first, ...rest] = wrap(description, budget);
1539
+ // A name wider than the column takes its own line rather than shoving the
1540
+ // description out — the top-level help has several of these today.
1541
+ if (name.length > nameWidth) {
1542
+ out.push(`${INDENT}${name}`);
1543
+ for (const line of [first, ...rest]) {
1544
+ if (line !== undefined) out.push(`${INDENT}${" ".repeat(nameWidth)} ${c.dim(line)}`);
1545
+ }
1546
+ continue;
1547
+ }
1548
+ out.push(`${INDENT}${name.padEnd(nameWidth)} ${c.dim(first ?? "")}`);
1549
+ for (const line of rest) out.push(`${INDENT}${" ".repeat(nameWidth)} ${c.dim(line)}`);
1550
+ }
1551
+ return out;
1552
+ };
1553
+ return stack(
1554
+ spec.lead ? spec.lead.map((l) => `${INDENT}${l}`) : null,
1555
+ section("USAGE", entryLines(spec.usage)),
1556
+ section("OPTIONS", entryLines(spec.options ?? [])),
1557
+ section("EXAMPLES", (spec.examples ?? []).map((e) => `${INDENT}${e}`)),
1558
+ );
1559
+ }