blume 2.0.2 → 2.1.0

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 (601) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +113 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-6k4ftwze.js → chunk-0gz36akr.js} +18 -2
  5. package/dist/cli/{chunk-6k4ftwze.js.map → chunk-0gz36akr.js.map} +3 -3
  6. package/dist/cli/{chunk-zp79m0ts.js → chunk-1n228xqb.js} +2 -2
  7. package/dist/cli/{chunk-ddndchfr.js → chunk-2f3ndkpw.js} +57 -29
  8. package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
  9. package/dist/cli/{chunk-fsmrqk8a.js → chunk-2f7n4qzt.js} +8 -2
  10. package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-2f7n4qzt.js.map} +3 -3
  11. package/dist/cli/{chunk-ppzjqwx2.js → chunk-2n4ps1b3.js} +17 -18
  12. package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-2n4ps1b3.js.map} +3 -3
  13. package/dist/cli/{chunk-5r8g91qn.js → chunk-43zcf4gz.js} +398 -159
  14. package/dist/cli/chunk-43zcf4gz.js.map +36 -0
  15. package/dist/cli/chunk-4ejxbzee.js +39 -0
  16. package/dist/cli/chunk-4ejxbzee.js.map +10 -0
  17. package/dist/cli/{chunk-hr8ne106.js → chunk-4h8z9vv5.js} +296 -77
  18. package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
  19. package/dist/cli/{chunk-k7pj68a8.js → chunk-5cg2rzgf.js} +18 -16
  20. package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5cg2rzgf.js.map} +2 -2
  21. package/dist/cli/{chunk-h2ez8dzb.js → chunk-5ffywsmb.js} +4 -4
  22. package/dist/cli/{chunk-g698a744.js → chunk-5wj2qwn2.js} +5 -5
  23. package/dist/cli/chunk-6bm0xpgd.js +18 -0
  24. package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
  25. package/dist/cli/{chunk-3yce002v.js → chunk-713mngdq.js} +15 -8
  26. package/dist/cli/chunk-713mngdq.js.map +14 -0
  27. package/dist/cli/{chunk-91ws1n6j.js → chunk-9geymqns.js} +17 -15
  28. package/dist/cli/{chunk-91ws1n6j.js.map → chunk-9geymqns.js.map} +2 -2
  29. package/dist/cli/{chunk-6dtt0zfn.js → chunk-9xr4dgee.js} +27 -18
  30. package/dist/cli/chunk-9xr4dgee.js.map +10 -0
  31. package/dist/cli/{chunk-sqw4ekg1.js → chunk-amrb6hgh.js} +2 -2
  32. package/dist/cli/chunk-cm89scdv.js +443 -0
  33. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  34. package/dist/cli/{chunk-wdrt2k2v.js → chunk-cmtcnh9f.js} +90 -26
  35. package/dist/cli/chunk-cmtcnh9f.js.map +10 -0
  36. package/dist/cli/chunk-cy5bm0j0.js +104 -0
  37. package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
  38. package/dist/cli/{chunk-7mbqtmgb.js → chunk-e7vvk5ya.js} +21 -12
  39. package/dist/cli/chunk-e7vvk5ya.js.map +10 -0
  40. package/dist/cli/{chunk-qkb5a8sa.js → chunk-g2z9x01v.js} +6 -98
  41. package/dist/cli/chunk-g2z9x01v.js.map +10 -0
  42. package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
  43. package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
  44. package/dist/cli/{chunk-h7k3nq3v.js → chunk-gp84za0h.js} +2 -2
  45. package/dist/cli/{chunk-hqp2ajnh.js → chunk-h2a48yhd.js} +195 -73
  46. package/dist/cli/chunk-h2a48yhd.js.map +36 -0
  47. package/dist/cli/{chunk-pbg5a4s3.js → chunk-jnwj5kjj.js} +71 -47
  48. package/dist/cli/chunk-jnwj5kjj.js.map +19 -0
  49. package/dist/cli/chunk-kwrg63tm.js +31 -0
  50. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  51. package/dist/cli/{chunk-8g8ytmgx.js → chunk-nd1821sk.js} +21 -19
  52. package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-nd1821sk.js.map} +2 -2
  53. package/dist/cli/{chunk-w4bxdvsa.js → chunk-ntv4qrt7.js} +17 -15
  54. package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-ntv4qrt7.js.map} +2 -2
  55. package/dist/cli/{chunk-bfwp9vp6.js → chunk-p804f3pe.js} +7 -7
  56. package/dist/cli/{chunk-4e9b9ra6.js → chunk-py62fewh.js} +6 -6
  57. package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-py62fewh.js.map} +1 -1
  58. package/dist/cli/chunk-q3wxxq39.js +126 -0
  59. package/dist/cli/chunk-q3wxxq39.js.map +11 -0
  60. package/dist/cli/{chunk-j85scx15.js → chunk-rekqz4qg.js} +142 -46
  61. package/dist/cli/chunk-rekqz4qg.js.map +10 -0
  62. package/dist/cli/{chunk-00gs3wqs.js → chunk-ta27feev.js} +1 -1
  63. package/dist/cli/{chunk-v6ya5kcb.js → chunk-xkz5xbtv.js} +4031 -1132
  64. package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
  65. package/dist/cli/{chunk-bbnwccaz.js → chunk-y4ykbark.js} +2 -2
  66. package/dist/cli/{chunk-n1yg3tj3.js → chunk-y5h4r2y0.js} +6 -6
  67. package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-y5h4r2y0.js.map} +3 -3
  68. package/dist/cli/{chunk-nfcyttvj.js → chunk-yj17vs39.js} +9 -9
  69. package/dist/cli/{chunk-nfcyttvj.js.map → chunk-yj17vs39.js.map} +1 -1
  70. package/dist/cli/{chunk-7vtckvaw.js → chunk-yxmk4pec.js} +16 -14
  71. package/dist/cli/{chunk-7vtckvaw.js.map → chunk-yxmk4pec.js.map} +2 -2
  72. package/dist/cli/{chunk-gs7r695n.js → chunk-zccbjcj0.js} +22 -8
  73. package/dist/cli/{chunk-gs7r695n.js.map → chunk-zccbjcj0.js.map} +3 -3
  74. package/dist/cli/{chunk-xh43dwgw.js → chunk-zf748avv.js} +415 -225
  75. package/dist/cli/chunk-zf748avv.js.map +37 -0
  76. package/dist/cli/{chunk-d1v5rhy0.js → chunk-zhvd6adm.js} +16 -14
  77. package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-zhvd6adm.js.map} +2 -2
  78. package/dist/cli/index.js +28 -20
  79. package/dist/cli/index.js.map +5 -5
  80. package/dist/types/ai/agent-surface.d.ts +8 -7
  81. package/dist/types/ai/ai-catalog.d.ts +7 -0
  82. package/dist/types/ai/ask-context.d.ts +22 -0
  83. package/dist/types/ai/ask.d.ts +271 -58
  84. package/dist/types/ai/index.d.ts +2 -2
  85. package/dist/types/ai/link-headers.d.ts +8 -1
  86. package/dist/types/ai/openapi-components.d.ts +5 -2
  87. package/dist/types/ai/relative-links.d.ts +9 -7
  88. package/dist/types/ai/site-skill.d.ts +26 -0
  89. package/dist/types/ai/skills.d.ts +20 -1
  90. package/dist/types/ai/tar.d.ts +1 -3
  91. package/dist/types/analytics/index.d.ts +2 -0
  92. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  93. package/dist/types/analytics/schema.d.ts +14 -0
  94. package/dist/types/astro/integration.d.ts +11 -2
  95. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  96. package/dist/types/captcha/index.d.ts +28 -0
  97. package/dist/types/captcha/schema.d.ts +29 -0
  98. package/dist/types/captcha/turnstile.d.ts +28 -0
  99. package/dist/types/components/content/api-field.d.ts +59 -0
  100. package/dist/types/components/content/api-page.d.ts +95 -0
  101. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  102. package/dist/types/components/openapi/helpers.d.ts +137 -0
  103. package/dist/types/components/openapi/security.d.ts +85 -0
  104. package/dist/types/consent/ethyca.d.ts +48 -0
  105. package/dist/types/consent/index.d.ts +37 -0
  106. package/dist/types/consent/native.d.ts +28 -0
  107. package/dist/types/consent/osano.d.ts +42 -0
  108. package/dist/types/consent/schema.d.ts +110 -0
  109. package/dist/types/core/base-path.d.ts +21 -7
  110. package/dist/types/core/config-input.d.ts +196 -27
  111. package/dist/types/core/data.d.ts +49 -4
  112. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  113. package/dist/types/core/footer.d.ts +8 -0
  114. package/dist/types/core/heading-markers.d.ts +5 -7
  115. package/dist/types/core/i18n-ui.d.ts +86 -0
  116. package/dist/types/core/i18n.d.ts +2 -0
  117. package/dist/types/core/includes.d.ts +8 -2
  118. package/dist/types/core/last-modified.d.ts +10 -0
  119. package/dist/types/core/localizable.d.ts +7 -0
  120. package/dist/types/core/meta.d.ts +8 -0
  121. package/dist/types/core/page-modes.d.ts +29 -0
  122. package/dist/types/core/redirect-patterns.d.ts +144 -0
  123. package/dist/types/core/related.d.ts +40 -0
  124. package/dist/types/core/schema.d.ts +558 -56
  125. package/dist/types/core/sources/lower.d.ts +29 -16
  126. package/dist/types/core/sources/normalize.d.ts +13 -1
  127. package/dist/types/core/sources/read.d.ts +5 -0
  128. package/dist/types/core/sources/watch.d.ts +12 -5
  129. package/dist/types/core/standard-schema.d.ts +5 -0
  130. package/dist/types/core/types.d.ts +1 -1
  131. package/dist/types/core/variables.d.ts +42 -0
  132. package/dist/types/deploy/artifacts.d.ts +6 -4
  133. package/dist/types/deploy/headers.d.ts +5 -0
  134. package/dist/types/deploy/node-headers.d.ts +13 -2
  135. package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
  136. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  137. package/dist/types/deploy/platforms/types.d.ts +8 -0
  138. package/dist/types/deploy/platforms/vercel.d.ts +6 -0
  139. package/dist/types/deploy/redirects.d.ts +61 -22
  140. package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
  141. package/dist/types/markdown/code-title.d.ts +17 -0
  142. package/dist/types/markdown/directives.d.ts +62 -0
  143. package/dist/types/markdown/features.d.ts +24 -2
  144. package/dist/types/markdown/mdast.d.ts +63 -0
  145. package/dist/types/narration/build.d.ts +70 -0
  146. package/dist/types/narration/provider.d.ts +26 -0
  147. package/dist/types/narration/script.d.ts +154 -0
  148. package/dist/types/openapi/asyncapi.d.ts +4 -2
  149. package/dist/types/openapi/model.d.ts +12 -6
  150. package/dist/types/openapi/overlay.d.ts +54 -0
  151. package/dist/types/openapi/parse.d.ts +13 -0
  152. package/dist/types/openapi/references.d.ts +14 -4
  153. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  154. package/dist/types/ratelimit/index.d.ts +27 -0
  155. package/dist/types/ratelimit/memory.d.ts +36 -0
  156. package/dist/types/ratelimit/schema.d.ts +111 -0
  157. package/dist/types/ratelimit/upstash.d.ts +21 -0
  158. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  159. package/dist/types/reference/asyncapi.d.ts +10 -10
  160. package/dist/types/reference/graphql.d.ts +13 -10
  161. package/dist/types/reference/openapi.d.ts +33 -12
  162. package/dist/types/reference/options.d.ts +45 -5
  163. package/dist/types/reference/scalar.d.ts +13 -0
  164. package/dist/types/reference/schema.d.ts +60 -36
  165. package/dist/types/search/documents.d.ts +12 -1
  166. package/dist/types/search/orama-index.d.ts +4 -0
  167. package/dist/types/seo/metatags.d.ts +20 -0
  168. package/docs/02-deployment.mdx +44 -6
  169. package/docs/03-upgrading.mdx +4 -4
  170. package/docs/04-migrating.mdx +1 -1
  171. package/docs/08-faq.mdx +1 -1
  172. package/docs/advanced/changelog.mdx +2 -2
  173. package/docs/advanced/custom-pages.mdx +3 -3
  174. package/docs/advanced/skills.mdx +14 -0
  175. package/docs/cli/audit.mdx +38 -2
  176. package/docs/cli/evals.mdx +3 -3
  177. package/docs/cli/index.mdx +1 -0
  178. package/docs/cli/translate.mdx +3 -3
  179. package/docs/cli/version.mdx +1 -1
  180. package/docs/configuration/analytics.mdx +22 -1
  181. package/docs/configuration/assistant.mdx +188 -36
  182. package/docs/configuration/consent.mdx +100 -0
  183. package/docs/configuration/customization.mdx +6 -4
  184. package/docs/configuration/index.mdx +63 -5
  185. package/docs/configuration/meta.ts +3 -0
  186. package/docs/configuration/narration.mdx +119 -0
  187. package/docs/configuration/rate-limiting.mdx +69 -0
  188. package/docs/configuration/search.mdx +30 -2
  189. package/docs/configuration/theming.mdx +3 -1
  190. package/docs/content/components.mdx +107 -1
  191. package/docs/content/frontmatter.mdx +77 -1
  192. package/docs/content/i18n.mdx +40 -3
  193. package/docs/content/includes.mdx +16 -0
  194. package/docs/content/index.mdx +4 -2
  195. package/docs/content/islands.mdx +1 -1
  196. package/docs/content/meta.mdx +4 -1
  197. package/docs/content/meta.ts +1 -0
  198. package/docs/content/navigation.mdx +28 -6
  199. package/docs/content/sources/contentful.mdx +32 -0
  200. package/docs/content/sources/custom.mdx +39 -0
  201. package/docs/content/sources/github-releases.mdx +31 -0
  202. package/docs/content/sources/index.mdx +91 -0
  203. package/docs/content/sources/meta.ts +16 -0
  204. package/docs/content/sources/notion.mdx +32 -0
  205. package/docs/content/sources/obsidian.mdx +37 -0
  206. package/docs/content/sources/payload.mdx +30 -0
  207. package/docs/content/sources/remote-mdx.mdx +40 -0
  208. package/docs/content/sources/sanity.mdx +29 -0
  209. package/docs/content/sources/strapi.mdx +31 -0
  210. package/docs/content/syntax.mdx +81 -2
  211. package/docs/content/variables.mdx +41 -0
  212. package/docs/content/versioning.mdx +1 -1
  213. package/docs/discoverability/agent-discovery.mdx +30 -3
  214. package/docs/discoverability/index.mdx +3 -2
  215. package/docs/discoverability/llms-txt.mdx +1 -1
  216. package/docs/discoverability/markdown.mdx +2 -2
  217. package/docs/discoverability/metadata.mdx +17 -0
  218. package/docs/discoverability/open-graph.mdx +5 -3
  219. package/docs/discoverability/rss.mdx +1 -1
  220. package/docs/references/api-pages.mdx +74 -0
  221. package/docs/references/asyncapi.mdx +2 -2
  222. package/docs/references/graphql.mdx +3 -3
  223. package/docs/references/meta.ts +1 -1
  224. package/docs/references/openapi.mdx +106 -5
  225. package/docs/references/scalar.mdx +3 -1
  226. package/package.json +36 -1
  227. package/skills/blume/SKILL.md +3 -0
  228. package/skills/blume-migrate/SKILL.md +5 -5
  229. package/skills/blume-migrate/references/docusaurus.md +3 -3
  230. package/skills/blume-migrate/references/fumadocs.md +1 -1
  231. package/skills/blume-migrate/references/mintlify.md +37 -19
  232. package/skills/blume-migrate/references/nextra.md +3 -3
  233. package/skills/blume-migrate/references/starlight.md +2 -1
  234. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +67 -16
  235. package/skills/blume-write-skill/SKILL.md +82 -0
  236. package/src/ai/agent-readability.ts +4 -3
  237. package/src/ai/agent-surface.ts +9 -7
  238. package/src/ai/ai-catalog.ts +12 -5
  239. package/src/ai/api/handlers.ts +2 -2
  240. package/src/ai/api/spec.ts +2 -2
  241. package/src/ai/api-catalog.ts +6 -2
  242. package/src/ai/ask-context.ts +73 -20
  243. package/src/ai/ask-data.ts +6 -0
  244. package/src/ai/ask-tools.ts +142 -0
  245. package/src/ai/ask.ts +354 -76
  246. package/src/ai/changelog-markdown.ts +2 -2
  247. package/src/ai/component-markdown.ts +75 -0
  248. package/src/ai/cors.ts +2 -0
  249. package/src/ai/index.ts +12 -0
  250. package/src/ai/link-headers.ts +14 -5
  251. package/src/ai/llms.ts +24 -9
  252. package/src/ai/markdown.ts +2 -1
  253. package/src/ai/mcp/data.ts +6 -0
  254. package/src/ai/mcp/discovery.ts +2 -2
  255. package/src/ai/mcp/query.ts +2 -2
  256. package/src/ai/mcp/server.ts +2 -2
  257. package/src/ai/openapi-components.ts +31 -9
  258. package/src/ai/relative-links.ts +42 -26
  259. package/src/ai/serializers.ts +2 -1
  260. package/src/ai/site-skill.ts +274 -0
  261. package/src/ai/skills.ts +49 -3
  262. package/src/ai/tar.ts +139 -12
  263. package/src/ai/write-skill.ts +97 -0
  264. package/src/analytics/head.ts +4 -0
  265. package/src/analytics/index.ts +5 -0
  266. package/src/analytics/one-dollar-stats.ts +86 -0
  267. package/src/analytics/schema.ts +2 -0
  268. package/src/astro/generate.ts +133 -19
  269. package/src/astro/include-hmr.ts +31 -12
  270. package/src/astro/include-refresh.ts +19 -8
  271. package/src/astro/index.ts +1 -0
  272. package/src/astro/integration.ts +143 -14
  273. package/src/astro/module-types.ts +6 -0
  274. package/src/astro/runtime-modules.ts +28 -13
  275. package/src/astro/templates.ts +364 -67
  276. package/src/astro/variables.ts +31 -0
  277. package/src/audit/catalog.ts +19 -0
  278. package/src/audit/checks/contrast.ts +183 -0
  279. package/src/audit/checks/i18n.ts +2 -3
  280. package/src/audit/checks/indexability.ts +2 -3
  281. package/src/audit/checks/links.ts +13 -13
  282. package/src/audit/checks/sitemap.ts +2 -3
  283. package/src/audit/graph.ts +3 -1
  284. package/src/audit/redirects.ts +46 -15
  285. package/src/audit/run.ts +14 -9
  286. package/src/audit/snapshot.ts +5 -0
  287. package/src/audit/types.ts +2 -1
  288. package/src/blume-modules.d.ts +7 -0
  289. package/src/captcha/client.ts +152 -0
  290. package/src/captcha/hcaptcha.ts +36 -0
  291. package/src/captcha/index.ts +28 -0
  292. package/src/captcha/schema.ts +32 -0
  293. package/src/captcha/turnstile.ts +38 -0
  294. package/src/captcha/verify.ts +71 -0
  295. package/src/cli/args.ts +32 -0
  296. package/src/cli/command-meta.ts +5 -0
  297. package/src/cli/commands/audit.ts +4 -1
  298. package/src/cli/commands/doctor.ts +25 -17
  299. package/src/cli/commands/eval.ts +19 -11
  300. package/src/cli/commands/skill.ts +96 -0
  301. package/src/cli/commands/translate.ts +9 -12
  302. package/src/cli/commands/validate.ts +3 -1
  303. package/src/cli/dev-lock.ts +157 -37
  304. package/src/cli/index.ts +5 -0
  305. package/src/cli/report-format.ts +11 -6
  306. package/src/cli/required-secrets.ts +71 -29
  307. package/src/components/BlumePage.astro +2 -0
  308. package/src/components/colors.ts +19 -0
  309. package/src/components/content/ApiEndpoint.astro +18 -0
  310. package/src/components/content/ApiField.astro +79 -0
  311. package/src/components/content/ApiPlayground.astro +62 -0
  312. package/src/components/content/ApiRail.astro +19 -0
  313. package/src/components/content/Badge.astro +5 -3
  314. package/src/components/content/Color.astro +1 -0
  315. package/src/components/content/Component.astro +2 -2
  316. package/src/components/content/FileTree.astro +1 -0
  317. package/src/components/content/GithubInfo.astro +1 -0
  318. package/src/components/content/Panel.astro +1 -0
  319. package/src/components/content/ParamField.astro +25 -0
  320. package/src/components/content/RequestExample.astro +17 -0
  321. package/src/components/content/ResponseExample.astro +17 -0
  322. package/src/components/content/ResponseField.astro +32 -0
  323. package/src/components/content/Step.astro +1 -0
  324. package/src/components/content/Tab.astro +0 -1
  325. package/src/components/content/Tabs.astro +4 -0
  326. package/src/components/content/Tile.astro +1 -0
  327. package/src/components/content/Tooltip.astro +1 -0
  328. package/src/components/content/Tree.astro +1 -0
  329. package/src/components/content/TypeTable.astro +4 -1
  330. package/src/components/content/View.astro +21 -0
  331. package/src/components/content/ViewSwitcher.astro +168 -0
  332. package/src/components/content/api-field.ts +100 -0
  333. package/src/components/content/api-page.ts +224 -0
  334. package/src/components/content/badge-color.ts +5 -2
  335. package/src/components/content/base-href.ts +13 -36
  336. package/src/components/content/view-switcher.ts +71 -0
  337. package/src/components/index.ts +2 -0
  338. package/src/components/islands/Assistant.astro +10 -1
  339. package/src/components/islands/assistant.tsx +125 -18
  340. package/src/components/islands/base-path.ts +47 -10
  341. package/src/components/islands/code-question.ts +62 -0
  342. package/src/components/islands/hooks.ts +145 -40
  343. package/src/components/islands/support-link.ts +68 -0
  344. package/src/components/islands/webmcp.ts +4 -2
  345. package/src/components/layout/Analytics.astro +11 -2
  346. package/src/components/layout/Banner.astro +14 -10
  347. package/src/components/layout/Breadcrumbs.astro +11 -4
  348. package/src/components/layout/ConsentBanner.astro +62 -0
  349. package/src/components/layout/ConsentHead.astro +38 -0
  350. package/src/components/layout/Directory.astro +94 -0
  351. package/src/components/layout/DiscoveryLinks.astro +9 -5
  352. package/src/components/layout/DrawerSwitchers.astro +51 -0
  353. package/src/components/layout/Empty.astro +2 -2
  354. package/src/components/layout/Header.astro +124 -82
  355. package/src/components/layout/LanguageSwitcher.astro +50 -17
  356. package/src/components/layout/NarrationPlayer.astro +1085 -0
  357. package/src/components/layout/NavSelector.astro +35 -9
  358. package/src/components/layout/NavTabMenu.astro +3 -3
  359. package/src/components/layout/NavTree.astro +16 -6
  360. package/src/components/layout/PageActions.astro +4 -3
  361. package/src/components/layout/PageFeedback.astro +54 -49
  362. package/src/components/layout/PageLayout.astro +78 -9
  363. package/src/components/layout/Pagination.astro +3 -3
  364. package/src/components/layout/ReferenceLayout.astro +17 -6
  365. package/src/components/layout/RelatedPages.astro +42 -0
  366. package/src/components/layout/RootLayout.astro +313 -50
  367. package/src/components/layout/Search.astro +64 -30
  368. package/src/components/layout/SiteFooter.astro +124 -0
  369. package/src/components/layout/SiteMetatags.astro +16 -0
  370. package/src/components/layout/VersionBanner.astro +2 -2
  371. package/src/components/layout/analytics-client.ts +12 -0
  372. package/src/components/layout/consent/native.ts +79 -0
  373. package/src/components/layout/consent/types.ts +37 -0
  374. package/src/components/layout/footer-links.ts +53 -0
  375. package/src/components/layout/footer-socials.ts +87 -0
  376. package/src/components/layout/locale-redirect.ts +107 -0
  377. package/src/components/layout/nav-utils.ts +83 -0
  378. package/src/components/layout/page-feedback.ts +96 -0
  379. package/src/components/layout/search/analytics.ts +98 -0
  380. package/src/components/layout/search/flexsearch.ts +17 -5
  381. package/src/components/layout/search/orama-cloud.ts +26 -14
  382. package/src/components/layout/search/types.ts +20 -0
  383. package/src/components/layout/search/typesense.ts +5 -1
  384. package/src/components/layout/toc-active.ts +41 -0
  385. package/src/components/layout/toc-element.ts +8 -14
  386. package/src/components/openapi/ApiTagOperations.astro +2 -2
  387. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  388. package/src/components/openapi/GraphqlChip.astro +2 -2
  389. package/src/components/openapi/Operation.astro +163 -13
  390. package/src/components/openapi/Playground.astro +4 -4
  391. package/src/components/openapi/RequestBody.astro +6 -4
  392. package/src/components/openapi/RequestPanel.astro +63 -8
  393. package/src/components/openapi/Responses.astro +6 -4
  394. package/src/components/openapi/SchemaTable.astro +7 -0
  395. package/src/components/openapi/async-snippets.ts +4 -4
  396. package/src/components/openapi/async.ts +38 -6
  397. package/src/components/openapi/code-samples.ts +97 -0
  398. package/src/components/openapi/helpers.ts +19 -9
  399. package/src/components/openapi/message-composer.ts +6 -1
  400. package/src/components/openapi/message.ts +17 -2
  401. package/src/components/openapi/operation-model.ts +81 -11
  402. package/src/components/openapi/panel.ts +4 -2
  403. package/src/components/openapi/param-style.ts +181 -0
  404. package/src/components/openapi/playground-client.ts +73 -16
  405. package/src/components/openapi/request.ts +190 -26
  406. package/src/components/openapi/schema-tree.ts +30 -24
  407. package/src/components/openapi/security.ts +6 -3
  408. package/src/components/openapi/snippets.ts +953 -13
  409. package/src/components/openapi/ws-client.ts +18 -2
  410. package/src/components/props.ts +6 -0
  411. package/src/consent/client.ts +95 -0
  412. package/src/consent/clients.ts +19 -0
  413. package/src/consent/ethyca.ts +71 -0
  414. package/src/consent/head.ts +24 -0
  415. package/src/consent/index.ts +37 -0
  416. package/src/consent/init.ts +29 -0
  417. package/src/consent/native.ts +36 -0
  418. package/src/consent/osano.ts +59 -0
  419. package/src/consent/schema.ts +30 -0
  420. package/src/core/base-path.ts +62 -16
  421. package/src/core/builtin-tags.ts +5 -0
  422. package/src/core/client-address.ts +19 -0
  423. package/src/core/config-input.ts +203 -27
  424. package/src/core/data.ts +44 -4
  425. package/src/core/diagnostics.ts +5 -2
  426. package/src/core/directive-diagnostics.ts +99 -0
  427. package/src/core/footer.ts +26 -0
  428. package/src/core/frontmatter.ts +21 -18
  429. package/src/core/graph.ts +39 -36
  430. package/src/core/heading-markers.ts +5 -18
  431. package/src/core/i18n-ui.ts +82 -2
  432. package/src/core/i18n.ts +31 -3
  433. package/src/core/includes.ts +20 -3
  434. package/src/core/last-modified.ts +25 -3
  435. package/src/core/links.ts +16 -2
  436. package/src/core/locale-links.ts +5 -1
  437. package/src/core/localizable.ts +26 -0
  438. package/src/core/meta.ts +33 -19
  439. package/src/core/navigation.ts +71 -20
  440. package/src/core/page-modes.ts +56 -0
  441. package/src/core/project-graph.ts +138 -10
  442. package/src/core/redirect-patterns.ts +491 -0
  443. package/src/core/related.ts +131 -0
  444. package/src/core/schema.ts +342 -68
  445. package/src/core/sources/contentful-rich-text.ts +27 -20
  446. package/src/core/sources/filesystem.ts +20 -2
  447. package/src/core/sources/github-releases.ts +25 -6
  448. package/src/core/sources/lexical.ts +23 -18
  449. package/src/core/sources/lower.ts +201 -34
  450. package/src/core/sources/mdx-remote.ts +51 -16
  451. package/src/core/sources/normalize.ts +327 -91
  452. package/src/core/sources/notion.ts +52 -25
  453. package/src/core/sources/obsidian.ts +23 -6
  454. package/src/core/sources/portable-text.ts +38 -21
  455. package/src/core/sources/read.ts +8 -3
  456. package/src/core/sources/strapi-blocks.ts +20 -16
  457. package/src/core/sources/watch.ts +20 -7
  458. package/src/core/standard-schema.ts +10 -6
  459. package/src/core/types.ts +1 -1
  460. package/src/core/ui-packs/ar.ts +43 -0
  461. package/src/core/ui-packs/bg.ts +45 -0
  462. package/src/core/ui-packs/bn.ts +43 -0
  463. package/src/core/ui-packs/ca.ts +46 -0
  464. package/src/core/ui-packs/cs.ts +44 -0
  465. package/src/core/ui-packs/da.ts +45 -0
  466. package/src/core/ui-packs/de.ts +46 -0
  467. package/src/core/ui-packs/el.ts +45 -0
  468. package/src/core/ui-packs/es.ts +46 -0
  469. package/src/core/ui-packs/fa.ts +45 -0
  470. package/src/core/ui-packs/fi.ts +45 -0
  471. package/src/core/ui-packs/fr.ts +46 -0
  472. package/src/core/ui-packs/he.ts +43 -0
  473. package/src/core/ui-packs/hi.ts +44 -0
  474. package/src/core/ui-packs/hr.ts +45 -0
  475. package/src/core/ui-packs/hu.ts +44 -0
  476. package/src/core/ui-packs/id.ts +45 -0
  477. package/src/core/ui-packs/it.ts +45 -0
  478. package/src/core/ui-packs/ja.ts +45 -0
  479. package/src/core/ui-packs/ko.ts +44 -0
  480. package/src/core/ui-packs/nl.ts +46 -0
  481. package/src/core/ui-packs/no.ts +44 -0
  482. package/src/core/ui-packs/pl.ts +45 -0
  483. package/src/core/ui-packs/pt-br.ts +46 -0
  484. package/src/core/ui-packs/pt.ts +45 -0
  485. package/src/core/ui-packs/ro.ts +45 -0
  486. package/src/core/ui-packs/ru.ts +45 -0
  487. package/src/core/ui-packs/sk.ts +44 -0
  488. package/src/core/ui-packs/sr.ts +45 -0
  489. package/src/core/ui-packs/sv.ts +44 -0
  490. package/src/core/ui-packs/th.ts +43 -0
  491. package/src/core/ui-packs/tr.ts +45 -0
  492. package/src/core/ui-packs/uk.ts +45 -0
  493. package/src/core/ui-packs/vi.ts +45 -0
  494. package/src/core/ui-packs/zh-tw.ts +43 -0
  495. package/src/core/ui-packs/zh.ts +43 -0
  496. package/src/core/variables.ts +103 -0
  497. package/src/core/version-cut.ts +52 -15
  498. package/src/deploy/artifacts.ts +104 -14
  499. package/src/deploy/cloudflare-negotiation.ts +39 -17
  500. package/src/deploy/headers.ts +10 -5
  501. package/src/deploy/node-headers.ts +64 -10
  502. package/src/deploy/platforms/cloudflare.ts +44 -3
  503. package/src/deploy/platforms/netlify.ts +34 -1
  504. package/src/deploy/platforms/node.ts +1 -0
  505. package/src/deploy/platforms/static.ts +1 -0
  506. package/src/deploy/platforms/types.ts +8 -0
  507. package/src/deploy/platforms/vercel.ts +38 -2
  508. package/src/deploy/redirects.ts +139 -36
  509. package/src/deploy/robots.ts +2 -2
  510. package/src/deploy/rss.ts +4 -3
  511. package/src/deploy/sitemap.ts +7 -5
  512. package/src/deploy/vercel-negotiation.ts +36 -0
  513. package/src/markdown/api-rail.ts +259 -0
  514. package/src/markdown/base-links.ts +34 -36
  515. package/src/markdown/code-title.ts +35 -9
  516. package/src/markdown/directives.ts +242 -36
  517. package/src/markdown/features.ts +20 -2
  518. package/src/markdown/fence-meta.ts +2 -0
  519. package/src/markdown/include.ts +11 -1
  520. package/src/markdown/index.ts +35 -11
  521. package/src/markdown/math.ts +4 -4
  522. package/src/markdown/mdast.ts +5 -2
  523. package/src/markdown/relative-links.ts +12 -4
  524. package/src/markdown/variables.ts +56 -0
  525. package/src/markdown/views.ts +82 -0
  526. package/src/narration/build.ts +412 -0
  527. package/src/narration/player.ts +249 -0
  528. package/src/narration/provider.ts +29 -0
  529. package/src/narration/script.ts +592 -0
  530. package/src/og/card.ts +136 -11
  531. package/src/og/derive.ts +41 -31
  532. package/src/og/index.ts +1 -0
  533. package/src/openapi/asyncapi.ts +4 -2
  534. package/src/openapi/model.ts +77 -36
  535. package/src/openapi/overlay.ts +256 -0
  536. package/src/openapi/parse.ts +55 -2
  537. package/src/openapi/proxy.ts +63 -10
  538. package/src/openapi/references.ts +21 -5
  539. package/src/openapi/render-mdx.ts +46 -3
  540. package/src/openapi/scalar.ts +40 -6
  541. package/src/openapi/source.ts +1 -1
  542. package/src/ratelimit/cloudflare.ts +54 -0
  543. package/src/ratelimit/index.ts +30 -0
  544. package/src/ratelimit/memory.ts +45 -0
  545. package/src/ratelimit/runtime.ts +229 -0
  546. package/src/ratelimit/schema.ts +28 -0
  547. package/src/ratelimit/upstash.ts +30 -0
  548. package/src/ratelimit/wrangler.ts +70 -0
  549. package/src/reference/asyncapi.ts +2 -2
  550. package/src/reference/graphql.ts +5 -2
  551. package/src/reference/openapi.ts +21 -7
  552. package/src/reference/options.ts +51 -7
  553. package/src/reference/scalar.ts +33 -8
  554. package/src/registry/eject.ts +201 -28
  555. package/src/registry/registry.ts +7 -0
  556. package/src/search/adapters/version-scope.ts +30 -0
  557. package/src/search/documents.ts +93 -28
  558. package/src/search/orama-index.ts +28 -3
  559. package/src/search/pagefind-ranking.ts +39 -0
  560. package/src/search/popular.ts +2 -1
  561. package/src/search/sync/algolia.ts +15 -4
  562. package/src/search/sync/orama-cloud.ts +3 -0
  563. package/src/search/sync/typesense.ts +5 -0
  564. package/src/seo/jsonld.ts +7 -3
  565. package/src/seo/metatags.ts +67 -0
  566. package/src/theme/chrome-icons.ts +1 -0
  567. package/src/theme/color.ts +500 -0
  568. package/src/theme/entry.ts +56 -3
  569. package/src/theme/palette.ts +121 -45
  570. package/src/translate/meta.ts +68 -24
  571. package/src/translate/prompts.ts +1 -0
  572. package/src/translate/run.ts +3 -3
  573. package/src/translate/validate.ts +12 -3
  574. package/src/translate/work-list.ts +56 -17
  575. package/src/upgrade/upgrade.ts +4 -171
  576. package/dist/cli/chunk-3yce002v.js.map +0 -14
  577. package/dist/cli/chunk-5r8g91qn.js.map +0 -34
  578. package/dist/cli/chunk-6dtt0zfn.js.map +0 -10
  579. package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
  580. package/dist/cli/chunk-ddndchfr.js.map +0 -14
  581. package/dist/cli/chunk-esh98wmb.js +0 -23
  582. package/dist/cli/chunk-esh98wmb.js.map +0 -10
  583. package/dist/cli/chunk-hqp2ajnh.js.map +0 -35
  584. package/dist/cli/chunk-hr8ne106.js.map +0 -13
  585. package/dist/cli/chunk-j85scx15.js.map +0 -10
  586. package/dist/cli/chunk-mqc662a6.js +0 -75
  587. package/dist/cli/chunk-mqc662a6.js.map +0 -11
  588. package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
  589. package/dist/cli/chunk-qkb5a8sa.js.map +0 -10
  590. package/dist/cli/chunk-v6ya5kcb.js.map +0 -189
  591. package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
  592. package/dist/cli/chunk-xh43dwgw.js.map +0 -36
  593. package/docs/content/sources.mdx +0 -338
  594. /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-1n228xqb.js.map} +0 -0
  595. /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-5ffywsmb.js.map} +0 -0
  596. /package/dist/cli/{chunk-g698a744.js.map → chunk-5wj2qwn2.js.map} +0 -0
  597. /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-amrb6hgh.js.map} +0 -0
  598. /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-gp84za0h.js.map} +0 -0
  599. /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-p804f3pe.js.map} +0 -0
  600. /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-ta27feev.js.map} +0 -0
  601. /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-y4ykbark.js.map} +0 -0
@@ -28,9 +28,10 @@ import {
28
28
  computeWorkList,
29
29
  scanForTranslation,
30
30
  } from "../../translate/work-list.ts";
31
+ import { parseTimeoutSeconds } from "../args.ts";
31
32
  import { commandMeta } from "../command-meta.ts";
32
33
  import { reportInternalError } from "../internal-error.ts";
33
- import { flushStdout, logger, reportDiagnostics } from "../log.ts";
34
+ import { logger, reportDiagnostics } from "../log.ts";
34
35
 
35
36
  /** Wall-clock ceiling per file, in seconds. */
36
37
  const DEFAULT_TIMEOUT_S = DEFAULT_TRANSLATE_TIMEOUT_MS / 1000;
@@ -80,12 +81,7 @@ const parseFlags = (args: TranslateFlags): ParsedTranslateFlags => {
80
81
  );
81
82
  process.exit(1);
82
83
  }
83
- const timeoutS =
84
- args.timeout === undefined ? DEFAULT_TIMEOUT_S : Number(args.timeout);
85
- if (!Number.isInteger(timeoutS) || timeoutS <= 0) {
86
- logger.error(`Invalid --timeout "${args.timeout}" (whole seconds).`);
87
- process.exit(1);
88
- }
84
+ const timeoutS = parseTimeoutSeconds(args.timeout, DEFAULT_TIMEOUT_S);
89
85
  const concurrency =
90
86
  args.concurrency === undefined
91
87
  ? DEFAULT_CONCURRENCY
@@ -228,8 +224,9 @@ export const translateCommand = defineCommand({
228
224
  process.stdout.write(checkReportJson(workList));
229
225
  }
230
226
  if (hasDrift(workList)) {
231
- await flushStdout();
232
- process.exit(1);
227
+ // Set the code and return rather than `process.exit`, which waits for
228
+ // neither a piped stderr (the drift report) nor a piped stdout.
229
+ process.exitCode = 1;
233
230
  }
234
231
  return;
235
232
  }
@@ -290,9 +287,9 @@ export const translateCommand = defineCommand({
290
287
  }
291
288
  if (result.counts.failed > 0 || result.counts.partial > 0) {
292
289
  // The ledger write above already persisted every success, so a failed
293
- // rerun only retries what actually failed.
294
- await flushStdout();
295
- process.exit(1);
290
+ // rerun only retries what actually failed. Set the code rather than
291
+ // `process.exit`, which would cut off a piped stderr summary.
292
+ process.exitCode = 1;
296
293
  }
297
294
  } catch (error) {
298
295
  if (error instanceof BlumeError) {
@@ -119,7 +119,9 @@ export const validateCommand = defineCommand({
119
119
  logger.success("No broken links found.");
120
120
  }
121
121
  if (hadErrors || strictFailure) {
122
- process.exit(1);
122
+ // Set the code and return rather than `process.exit`, which doesn't wait
123
+ // for a piped stderr: a long diagnostic list would be cut off.
124
+ process.exitCode = 1;
123
125
  }
124
126
  },
125
127
  });
@@ -1,8 +1,11 @@
1
1
  import {
2
2
  existsSync,
3
+ linkSync,
3
4
  mkdirSync,
4
5
  readFileSync,
6
+ renameSync,
5
7
  rmSync,
8
+ statSync,
6
9
  writeFileSync,
7
10
  } from "node:fs";
8
11
 
@@ -29,6 +32,83 @@ export interface DevLockInfo {
29
32
 
30
33
  const lockPath = (outDir: string): string => join(outDir, "dev.lock");
31
34
 
35
+ /** The mutex a process holds while it clears a stale lock. */
36
+ const reapPath = (outDir: string): string => join(outDir, "dev.lock.reap");
37
+
38
+ /**
39
+ * How long a lock file that doesn't parse counts as one being written right
40
+ * now. Blume creates a lock with its whole content in place (see
41
+ * {@link createExclusive}), but one left empty or cut off — by a crash, or by
42
+ * an older Blume that opened the file before writing it — would otherwise
43
+ * read as stale the instant it appears. Past this age it's debris.
44
+ */
45
+ const PARTIAL_LOCK_GRACE_MS = 2000;
46
+
47
+ /** Pause between claim attempts while another process is mid-claim. */
48
+ const RETRY_DELAY_MS = 5;
49
+
50
+ const pause = (ms: number): void => {
51
+ // A synchronous sleep: the claim is synchronous, and what it waits on is
52
+ // another process, so there's nothing in this one to yield to.
53
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
54
+ };
55
+
56
+ /** Per-process counter that keeps concurrent temp file names apart. */
57
+ let tempSequence = 0;
58
+
59
+ /** A private sibling of `path` to stage a write in. */
60
+ const tempPath = (path: string): string => {
61
+ tempSequence += 1;
62
+ return `${path}.${process.pid}.${tempSequence}.tmp`;
63
+ };
64
+
65
+ /**
66
+ * Create `path` holding `payload`, atomically and exclusively. The payload
67
+ * goes to a private temp file first, which is then hard-linked into place:
68
+ * the link fails when `path` exists, exactly like a `wx` open, but the file
69
+ * appears with its whole content — no reader ever sees it empty or half
70
+ * written. Returns false when `path` already exists.
71
+ */
72
+ const createExclusive = (path: string, payload: string): boolean => {
73
+ const temp = tempPath(path);
74
+ writeFileSync(temp, payload);
75
+ try {
76
+ linkSync(temp, path);
77
+ return true;
78
+ } catch (error) {
79
+ // SAFETY: `linkSync` failures are errno exceptions.
80
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
81
+ throw error;
82
+ }
83
+ return false;
84
+ } finally {
85
+ rmSync(temp, { force: true });
86
+ }
87
+ };
88
+
89
+ /** Replace `path` with `payload` in one step, so readers see old or new. */
90
+ const replaceFile = (path: string, payload: string): void => {
91
+ const temp = tempPath(path);
92
+ writeFileSync(temp, payload);
93
+ renameSync(temp, path);
94
+ };
95
+
96
+ /** A file's text, or null when it doesn't exist (anymore). */
97
+ const readIfExists = (
98
+ path: string
99
+ ): { raw: string; mtimeMs: number } | null => {
100
+ try {
101
+ const raw = readFileSync(path, "utf-8");
102
+ return { mtimeMs: statSync(path).mtimeMs, raw };
103
+ } catch (error) {
104
+ // SAFETY: `readFileSync`/`statSync` failures are errno exceptions.
105
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
106
+ throw error;
107
+ }
108
+ return null;
109
+ }
110
+ };
111
+
32
112
  /** What `JSON.parse` can yield for a lock file body. */
33
113
  type LockFileValue =
34
114
  | string
@@ -85,17 +165,44 @@ const isProcessAlive = (pid: number): boolean => {
85
165
  }
86
166
  };
87
167
 
168
+ /** What the lock file on a dir says, read once. */
169
+ type LockState =
170
+ | { kind: "absent" }
171
+ /** Unparseable but brand new: another process is writing it right now. */
172
+ | { kind: "busy" }
173
+ | { kind: "live"; lock: DevLockInfo }
174
+ /** A dead holder's lock, or unparseable debris. */
175
+ | { kind: "stale" };
176
+
177
+ const inspectLock = (outDir: string): LockState => {
178
+ const file = readIfExists(lockPath(outDir));
179
+ if (!file) {
180
+ return { kind: "absent" };
181
+ }
182
+ const lock = parseLock(file.raw);
183
+ if (!lock) {
184
+ return Date.now() - file.mtimeMs < PARTIAL_LOCK_GRACE_MS
185
+ ? { kind: "busy" }
186
+ : { kind: "stale" };
187
+ }
188
+ return isProcessAlive(lock.pid) ? { kind: "live", lock } : { kind: "stale" };
189
+ };
190
+
191
+ /**
192
+ * Whether this process may clear the lock to claim the dir: a dead holder's
193
+ * lock and debris, or this process's own leftover.
194
+ */
195
+ const isClaimable = (state: LockState): boolean =>
196
+ state.kind === "stale" ||
197
+ (state.kind === "live" && state.lock.pid === process.pid);
198
+
88
199
  /**
89
200
  * Read the lock on `outDir` held by a live `blume dev`, or null. A lock left
90
201
  * by a process that has since exited (stale) is treated as absent.
91
202
  */
92
203
  export const readDevLock = (outDir: string): DevLockInfo | null => {
93
- const path = lockPath(outDir);
94
- if (!existsSync(path)) {
95
- return null;
96
- }
97
- const lock = parseLock(readFileSync(path, "utf-8"));
98
- return lock && isProcessAlive(lock.pid) ? lock : null;
204
+ const state = inspectLock(outDir);
205
+ return state.kind === "live" ? state.lock : null;
99
206
  };
100
207
 
101
208
  /** Whether another live `blume dev` holds the lock on `outDir`. */
@@ -110,10 +217,6 @@ const lockPayload = (port?: number): string => {
110
217
  return JSON.stringify(info);
111
218
  };
112
219
 
113
- const writeLock = (outDir: string, port?: number): void => {
114
- writeFileSync(lockPath(outDir), lockPayload(port));
115
- };
116
-
117
220
  /** Thrown when another live `blume dev` already holds the lock. */
118
221
  export class DevLockHeldError extends Error {
119
222
  readonly lock: DevLockInfo;
@@ -138,43 +241,58 @@ const ownsLock = (outDir: string): boolean => {
138
241
  };
139
242
 
140
243
  /**
141
- * One atomic claim attempt. Returns `true` when this process wins the `wx`
142
- * write, `false` when the dir held a stale or our-own leftover lock (now
143
- * cleared, so the caller should retry). Throws {@link DevLockHeldError} when a
144
- * live foreign holder owns the dir.
244
+ * Clear a claimable lock without racing anyone else doing the same. Two
245
+ * processes can both read the same stale lock; if each simply deleted the
246
+ * file, the slower one could delete the lock the faster one had claimed in
247
+ * between, and both would win. So removing a lock takes the reap mutex
248
+ * (created the same atomic way), and whoever holds it reads the lock again
249
+ * first: a live lock claimed since is left alone. A mutex whose holder died
250
+ * mid-reap is cleared for the next attempt.
145
251
  */
146
- const tryClaimLock = (outDir: string, port?: number): boolean => {
147
- try {
148
- writeFileSync(lockPath(outDir), lockPayload(port), { flag: "wx" });
149
- return true;
150
- } catch (error) {
151
- // SAFETY: `writeFileSync` failures are errno exceptions.
152
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
153
- throw error;
252
+ const reapStaleLock = (outDir: string): void => {
253
+ const mutex = reapPath(outDir);
254
+ if (!createExclusive(mutex, lockPayload())) {
255
+ const holder = readIfExists(mutex);
256
+ const pid = holder ? parseLock(holder.raw)?.pid : undefined;
257
+ if (pid !== undefined && pid !== process.pid && isProcessAlive(pid)) {
258
+ pause(RETRY_DELAY_MS);
259
+ } else if (holder) {
260
+ rmSync(mutex, { force: true });
154
261
  }
155
- const existing = readDevLock(outDir);
156
- if (existing && existing.pid !== process.pid) {
157
- throw new DevLockHeldError(existing);
262
+ return;
263
+ }
264
+ try {
265
+ if (isClaimable(inspectLock(outDir))) {
266
+ rmSync(lockPath(outDir), { force: true });
158
267
  }
159
- // Stale or our own leftover: clear it and race for the claim again.
160
- rmSync(lockPath(outDir), { force: true });
161
- return false;
268
+ } finally {
269
+ rmSync(mutex, { force: true });
162
270
  }
163
271
  };
164
272
 
165
273
  /**
166
274
  * Write the current process's dev lock into `outDir` and return a release
167
- * function. The claim is atomic (`wx`): two `blume dev` processes racing the
168
- * same dir can't both pass a check-then-write — the loser gets a
169
- * {@link DevLockHeldError} naming the live holder. A stale lock (dead pid) or
170
- * this process's own leftover is cleared and re-claimed. The release only
171
- * removes the file if it's still ours, so a newer dev server's lock is never
172
- * clobbered.
275
+ * function. Two `blume dev` processes starting at the same moment can't both
276
+ * win: the lock appears whole and exclusively (a hard link, which fails when
277
+ * the file exists), a lock still being written is waited on rather than read
278
+ * as stale, and a stale lock (dead pid) or this process's own leftover is only
279
+ * cleared under the reap mutex — the loser gets a {@link DevLockHeldError}
280
+ * naming the live holder. The release only removes the file if it's still
281
+ * ours, so a newer dev server's lock is never clobbered.
173
282
  */
174
283
  export const acquireDevLock = (outDir: string, port?: number): (() => void) => {
175
284
  mkdirSync(outDir, { recursive: true });
176
- while (!tryClaimLock(outDir, port)) {
177
- // Retry until we win the claim or a live holder makes us throw.
285
+ const payload = lockPayload(port);
286
+ while (!createExclusive(lockPath(outDir), payload)) {
287
+ const state = inspectLock(outDir);
288
+ if (isClaimable(state)) {
289
+ reapStaleLock(outDir);
290
+ } else if (state.kind === "live") {
291
+ throw new DevLockHeldError(state.lock);
292
+ } else if (state.kind === "busy") {
293
+ pause(RETRY_DELAY_MS);
294
+ }
295
+ // "absent": the holder released it just now — race for the claim again.
178
296
  }
179
297
  let released = false;
180
298
  return () => {
@@ -199,7 +317,9 @@ export const acquireDevLock = (outDir: string, port?: number): (() => void) => {
199
317
  */
200
318
  export const updateDevLockPort = (outDir: string, port: number): void => {
201
319
  if (ownsLock(outDir)) {
202
- writeLock(outDir, port);
320
+ // Replaced in one step: rewriting in place would leave a moment where a
321
+ // concurrent claim reads an empty lock.
322
+ replaceFile(lockPath(outDir), lockPayload(port));
203
323
  }
204
324
  };
205
325
 
package/src/cli/index.ts CHANGED
@@ -81,6 +81,11 @@ const main = defineCommand({
81
81
  () => import("./commands/preview.ts"),
82
82
  "previewCommand"
83
83
  ),
84
+ skill: lazyCommand(
85
+ commandMeta.skill,
86
+ () => import("./commands/skill.ts"),
87
+ "skillCommand"
88
+ ),
84
89
  sync: lazyCommand(
85
90
  commandMeta.sync,
86
91
  () => import("./commands/sync.ts"),
@@ -11,12 +11,17 @@ export const seconds = (ms: number): string => `${(ms / 1000).toFixed(1)}s`;
11
11
  export const money = (cost: number | undefined): string =>
12
12
  cost === undefined ? "" : `$${cost.toFixed(2)}`;
13
13
 
14
- /** Milliseconds as `12.3s` under a minute, `4m 12s` from there up. */
14
+ /**
15
+ * Milliseconds as `12.3s` under a minute, `4m 12s` from there up. Rounding
16
+ * happens before the split, so a value just under a boundary carries over
17
+ * (`59_990` → `1m 0s`, `119_700` → `2m 0s`) instead of printing `60.0s` or
18
+ * `1m 60s`.
19
+ */
15
20
  export const duration = (ms: number): string => {
16
- if (ms < 60_000) {
17
- return seconds(ms);
21
+ const tenths = Math.round(ms / 100);
22
+ if (tenths < 600) {
23
+ return `${(tenths / 10).toFixed(1)}s`;
18
24
  }
19
- const minutes = Math.floor(ms / 60_000);
20
- const rest = Math.round((ms % 60_000) / 1000);
21
- return `${minutes}m ${rest}s`;
25
+ const total = Math.round(ms / 1000);
26
+ return `${Math.floor(total / 60)}m ${total % 60}s`;
22
27
  };
@@ -2,6 +2,60 @@ import { resolveAskBackend } from "../ai/ask.ts";
2
2
  import type { ResolvedConfig } from "../core/schema.ts";
3
3
  import type { Diagnostic } from "../core/types.ts";
4
4
 
5
+ /** A feature, and the secrets the adapter behind it reads. */
6
+ interface SecretOwner {
7
+ feature: string;
8
+ note?: string;
9
+ secrets: string[];
10
+ }
11
+
12
+ /**
13
+ * The adapters that name their own secrets. The built-ins mostly declare
14
+ * none: analytics ships public tokens in the page, consent managers load
15
+ * public scripts, reference adapters read public specs, and host adapters
16
+ * read platform-injected env.
17
+ */
18
+ const adapterSecretOwners = (config: ResolvedConfig): SecretOwner[] => {
19
+ const { consent, deployment, rateLimit } = config;
20
+ const { provider } = config.search;
21
+ const owners: SecretOwner[] = [
22
+ // Each search adapter declares the secrets its generated runtime reads.
23
+ { feature: `Search (${provider.kind})`, secrets: provider.requiredSecrets },
24
+ ...config.analytics.map((adapter) => ({
25
+ feature: `Analytics (${adapter.kind})`,
26
+ secrets: adapter.requiredSecrets,
27
+ })),
28
+ // Each content source adapter declares the env vars its fetch reads.
29
+ ...config.content.sources.map((source) => ({
30
+ feature: `Content source (${source.kind})`,
31
+ secrets: source.requiredSecrets,
32
+ })),
33
+ ...config.reference.map((adapter) => ({
34
+ feature: `API reference (${adapter.kind})`,
35
+ secrets: adapter.requiredSecrets,
36
+ })),
37
+ {
38
+ feature: `Deployment (${deployment.kind})`,
39
+ secrets: deployment.requiredSecrets,
40
+ },
41
+ ];
42
+ if (consent) {
43
+ owners.push({
44
+ feature: `Consent (${consent.kind})`,
45
+ secrets: consent.requiredSecrets,
46
+ });
47
+ }
48
+ // Upstash's endpoint and token.
49
+ if (rateLimit) {
50
+ owners.push({
51
+ feature: `Rate limiting (${rateLimit.kind})`,
52
+ note: "read at request time; without it the routes count in memory",
53
+ secrets: rateLimit.requiredSecrets,
54
+ });
55
+ }
56
+ return owners;
57
+ };
58
+
5
59
  /**
6
60
  * Warn early when an enabled feature needs a secret env var that isn't set, so
7
61
  * the failure surfaces at `blume dev`/`build` instead of at the first request in
@@ -26,47 +80,35 @@ export const checkRequiredSecrets = (config: ResolvedConfig): Diagnostic[] => {
26
80
 
27
81
  if (config.ai.assistant?.enabled && !config.ai.assistant.endpoint) {
28
82
  // The adapter descriptor names the env vars its route reads.
29
- const { provider } = config.ai.assistant;
83
+ const { captcha, provider } = config.ai.assistant;
30
84
  const backend = resolveAskBackend(provider);
31
85
  for (const env of provider.requiredSecrets) {
32
86
  requireSecret(`Assistant (${backend.label})`, env, backend.secretNote);
33
87
  }
34
- }
35
-
36
- // Each search adapter declares the secrets its generated runtime reads.
37
- const { provider } = config.search;
38
- for (const env of provider.requiredSecrets) {
39
- requireSecret(`Search (${provider.kind})`, env);
40
- }
41
-
42
- // Adapters name their own secrets; the built-in analytics adapters ship
43
- // public tokens in the page and declare none.
44
- for (const adapter of config.analytics) {
45
- for (const env of adapter.requiredSecrets) {
46
- requireSecret(`Analytics (${adapter.kind})`, env);
88
+ // The bot check's secret key, which the route verifies tokens with.
89
+ for (const env of captcha?.requiredSecrets ?? []) {
90
+ requireSecret(`Assistant bot check (${captcha?.kind})`, env);
47
91
  }
48
92
  }
49
93
 
50
- // Each content source adapter declares the env vars its fetch reads.
51
- for (const source of config.content.sources) {
52
- for (const env of source.requiredSecrets) {
53
- requireSecret(`Content source (${source.kind})`, env);
94
+ // Generated narration reads its key at build, not at runtime; without it the
95
+ // build skips the audio and pages read with browser voices.
96
+ const { narration } = config;
97
+ if (narration.enabled && narration.provider) {
98
+ for (const env of narration.provider.requiredSecrets) {
99
+ requireSecret(
100
+ "Narration audio",
101
+ env,
102
+ "read at build; without it pages use browser voices"
103
+ );
54
104
  }
55
105
  }
56
106
 
57
- // Adapters name their own secrets; the built-in reference adapters read
58
- // public specs and declare none.
59
- for (const adapter of config.reference) {
60
- for (const env of adapter.requiredSecrets) {
61
- requireSecret(`API reference (${adapter.kind})`, env);
107
+ for (const owner of adapterSecretOwners(config)) {
108
+ for (const env of owner.secrets) {
109
+ requireSecret(owner.feature, env, owner.note);
62
110
  }
63
111
  }
64
112
 
65
- // The deployment adapter names its own secrets; the built-in host adapters
66
- // read platform-injected env and declare none.
67
- for (const env of config.deployment.requiredSecrets) {
68
- requireSecret(`Deployment (${config.deployment.kind})`, env);
69
- }
70
-
71
113
  return diagnostics;
72
114
  };
@@ -49,6 +49,7 @@ import TreeRoot from "./content/Tree.astro";
49
49
  import TreeFile from "./content/TreeFile.astro";
50
50
  import TreeFolder from "./content/TreeFolder.astro";
51
51
  import TypeTable from "./content/TypeTable.astro";
52
+ import View from "./content/View.astro";
52
53
  import Visibility from "./content/Visibility.astro";
53
54
  import YouTube from "./content/YouTube.astro";
54
55
 
@@ -96,6 +97,7 @@ const components = {
96
97
  Tooltip,
97
98
  Tree,
98
99
  TypeTable,
100
+ View,
99
101
  Visibility,
100
102
  YouTube,
101
103
  ...extra,
@@ -49,6 +49,25 @@ export const STROKE = {
49
49
  yellow: "border-yellow-500/40 text-yellow-800 dark:text-yellow-300",
50
50
  } satisfies Record<Hue, string>;
51
51
 
52
+ /**
53
+ * The theme accent's label color: the accent mixed halfway toward the
54
+ * foreground. The accent is whatever the site configures (a preset, any CSS
55
+ * color, or the neutral black/white default), so no fixed shade can promise
56
+ * the contrast rule above, and the raw accent as text misses it (every preset
57
+ * sits under 3.5:1 over its tint in light mode). Pulling it toward the
58
+ * foreground darkens it in light mode and lightens it in dark, since the
59
+ * foreground contrasts with the page by construction: every preset clears
60
+ * 7:1 over its 15% tint in both modes.
61
+ */
62
+ const ACCENT_LABEL =
63
+ "text-[color-mix(in_oklab,var(--blume-accent)_50%,var(--blume-foreground))]";
64
+
65
+ /** `TINT` for the theme accent (`<Badge variant="accent">`). */
66
+ export const ACCENT_TINT = `bg-accent/15 ${ACCENT_LABEL}`;
67
+
68
+ /** `STROKE` for the theme accent. */
69
+ export const ACCENT_STROKE = `border-accent/40 ${ACCENT_LABEL}`;
70
+
52
71
  /** The neutral fallback every table falls through to. */
53
72
  export const MUTED = "bg-muted text-muted-foreground";
54
73
 
@@ -0,0 +1,18 @@
1
+ ---
2
+ // The method and path at the top of a page with `api` frontmatter, as an
3
+ // OpenAPI operation page shows them. `markdown/api-rail.ts` places it.
4
+ import MethodBadge from "../openapi/MethodBadge.astro";
5
+ import { readEndpointSpec } from "./api-page.ts";
6
+
7
+ interface Props {
8
+ /** The endpoint, as the page's plugin wrote it. */
9
+ spec: string;
10
+ }
11
+
12
+ const { method, target } = readEndpointSpec(Astro.props.spec);
13
+ ---
14
+
15
+ <div class="not-prose mb-6 flex flex-wrap items-center gap-3">
16
+ <MethodBadge method={method} />
17
+ <code class="break-all font-mono text-foreground text-sm">{target}</code>
18
+ </div>
@@ -0,0 +1,79 @@
1
+ ---
2
+ // The row behind `<ParamField>` and `<ResponseField>`: the name with its
3
+ // labels, location, type, and flags, the default, then the description from
4
+ // the slot, which may hold any MDX, nested fields in an `<Expandable>`
5
+ // included. Styled like the OpenAPI reference's rows, so a hand-written API
6
+ // page reads like a generated one. Narration skips it, as it does type tables.
7
+ import { defaultLabel, isOn } from "./api-field.ts";
8
+ import type { FieldValue, FlagProp } from "./api-field.ts";
9
+
10
+ interface Props {
11
+ default?: FieldValue;
12
+ deprecated?: FlagProp;
13
+ location?: string;
14
+ name?: string;
15
+ post?: string[];
16
+ pre?: string[];
17
+ required?: FlagProp;
18
+ type?: string;
19
+ }
20
+
21
+ const {
22
+ default: value,
23
+ deprecated,
24
+ location,
25
+ name,
26
+ post = [],
27
+ pre = [],
28
+ required,
29
+ type,
30
+ } = Astro.props;
31
+ const shownDefault = defaultLabel(value);
32
+ const labelClass =
33
+ "rounded bg-muted px-1.5 py-0.5 font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide";
34
+ ---
35
+
36
+ <div
37
+ class="border-border border-b py-4"
38
+ data-blume-field
39
+ data-blume-narration="skip"
40
+ >
41
+ <div class="not-prose flex flex-wrap items-baseline gap-x-2 gap-y-1">
42
+ {pre.map((label) => <span class={labelClass}>{label}</span>)}
43
+ {name && <code class="font-mono text-foreground text-sm">{name}</code>}
44
+ {post.map((label) => <span class={labelClass}>{label}</span>)}
45
+ {location && <span class={labelClass}>{location}</span>}
46
+ {type && <span class="text-muted-foreground text-xs">{type}</span>}
47
+ {
48
+ isOn(required) && (
49
+ <span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">
50
+ required
51
+ </span>
52
+ )
53
+ }
54
+ {
55
+ isOn(deprecated) && (
56
+ <span class="font-medium text-[0.625rem] text-muted-foreground uppercase tracking-wide line-through">
57
+ deprecated
58
+ </span>
59
+ )
60
+ }
61
+ </div>
62
+ {
63
+ shownDefault !== null && (
64
+ <div class="not-prose mt-2 text-muted-foreground text-xs">
65
+ Default:{" "}
66
+ <code class="rounded bg-muted px-1 py-0.5 text-foreground">
67
+ {shownDefault}
68
+ </code>
69
+ </div>
70
+ )
71
+ }
72
+ {
73
+ Astro.slots.has("default") && (
74
+ <div class="mt-2 text-muted-foreground text-sm [&>:first-child]:mt-0 [&>:last-child]:mb-0">
75
+ <slot />
76
+ </div>
77
+ )
78
+ }
79
+ </div>