blume 1.6.4 → 1.6.6

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 (178) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/bin/blume.mjs +3 -2
  3. package/dist/cli/chunk-0ewz4trd.js +679 -0
  4. package/dist/cli/chunk-0ewz4trd.js.map +15 -0
  5. package/dist/cli/chunk-27gtm2ym.js +69 -0
  6. package/dist/cli/chunk-27gtm2ym.js.map +11 -0
  7. package/dist/cli/chunk-2aj8ddew.js +72 -0
  8. package/dist/cli/chunk-2aj8ddew.js.map +10 -0
  9. package/dist/cli/chunk-3k0kzs6d.js +69 -0
  10. package/dist/cli/chunk-3k0kzs6d.js.map +11 -0
  11. package/dist/cli/chunk-3r94j3tc.js +221 -0
  12. package/dist/cli/chunk-3r94j3tc.js.map +10 -0
  13. package/dist/cli/chunk-4trphnvy.js +102 -0
  14. package/dist/cli/chunk-4trphnvy.js.map +11 -0
  15. package/dist/cli/chunk-4xyggvgf.js +21 -0
  16. package/dist/cli/chunk-4xyggvgf.js.map +10 -0
  17. package/dist/cli/chunk-5hs6gb7n.js +32 -0
  18. package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
  19. package/dist/cli/chunk-5yvt556e.js +185 -0
  20. package/dist/cli/chunk-5yvt556e.js.map +11 -0
  21. package/dist/cli/chunk-62qsssnh.js +3808 -0
  22. package/dist/cli/chunk-62qsssnh.js.map +36 -0
  23. package/dist/cli/chunk-6kzzpsx8.js +26 -0
  24. package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
  25. package/dist/cli/chunk-8gnpdsn1.js +952 -0
  26. package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
  27. package/dist/cli/chunk-9sh49q0h.js +30 -0
  28. package/dist/cli/chunk-9sh49q0h.js.map +10 -0
  29. package/dist/cli/chunk-aerwpe14.js +2370 -0
  30. package/dist/cli/chunk-aerwpe14.js.map +15 -0
  31. package/dist/cli/chunk-ag1zyr5x.js +176 -0
  32. package/dist/cli/chunk-ag1zyr5x.js.map +10 -0
  33. package/dist/cli/chunk-bawgnt8x.js +277 -0
  34. package/dist/cli/chunk-bawgnt8x.js.map +11 -0
  35. package/dist/cli/chunk-bcy492zc.js +16 -0
  36. package/dist/cli/chunk-bcy492zc.js.map +10 -0
  37. package/dist/cli/chunk-btfr9yvw.js +41 -0
  38. package/dist/cli/chunk-btfr9yvw.js.map +10 -0
  39. package/dist/cli/chunk-cbjnx4s8.js +73 -0
  40. package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
  41. package/dist/cli/chunk-cnvm6k3e.js +96 -0
  42. package/dist/cli/chunk-cnvm6k3e.js.map +10 -0
  43. package/dist/cli/chunk-etsqspj6.js +5170 -0
  44. package/dist/cli/chunk-etsqspj6.js.map +47 -0
  45. package/dist/cli/chunk-ev67ycx0.js +15 -0
  46. package/dist/cli/chunk-ev67ycx0.js.map +10 -0
  47. package/dist/cli/chunk-ey89bjj1.js +209 -0
  48. package/dist/cli/chunk-ey89bjj1.js.map +11 -0
  49. package/dist/cli/chunk-f75cqye8.js +76 -0
  50. package/dist/cli/chunk-f75cqye8.js.map +10 -0
  51. package/dist/cli/chunk-j00ezcg5.js +259 -0
  52. package/dist/cli/chunk-j00ezcg5.js.map +11 -0
  53. package/dist/cli/chunk-jtb45atp.js +467 -0
  54. package/dist/cli/chunk-jtb45atp.js.map +14 -0
  55. package/dist/cli/chunk-m3p3wahd.js +117 -0
  56. package/dist/cli/chunk-m3p3wahd.js.map +10 -0
  57. package/dist/cli/chunk-n0y172hf.js +387 -0
  58. package/dist/cli/chunk-n0y172hf.js.map +12 -0
  59. package/dist/cli/chunk-n4qjabmt.js +1062 -0
  60. package/dist/cli/chunk-n4qjabmt.js.map +25 -0
  61. package/dist/cli/chunk-nyqzjdhj.js +111 -0
  62. package/dist/cli/chunk-nyqzjdhj.js.map +11 -0
  63. package/dist/cli/chunk-pxj10x8y.js +35 -0
  64. package/dist/cli/chunk-pxj10x8y.js.map +10 -0
  65. package/dist/cli/chunk-s4jn7f1q.js +54 -0
  66. package/dist/cli/chunk-s4jn7f1q.js.map +10 -0
  67. package/dist/cli/chunk-s4k1pnvf.js +81 -0
  68. package/dist/cli/chunk-s4k1pnvf.js.map +10 -0
  69. package/dist/cli/chunk-s5e5jt53.js +227 -0
  70. package/dist/cli/chunk-s5e5jt53.js.map +11 -0
  71. package/dist/cli/chunk-sbdqrjbb.js +81 -0
  72. package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
  73. package/dist/cli/chunk-tc89yh2r.js +136 -0
  74. package/dist/cli/chunk-tc89yh2r.js.map +10 -0
  75. package/dist/cli/chunk-vt8fgygt.js +23 -0
  76. package/dist/cli/chunk-vt8fgygt.js.map +10 -0
  77. package/dist/cli/chunk-vv237fp3.js +1002 -0
  78. package/dist/cli/chunk-vv237fp3.js.map +13 -0
  79. package/dist/cli/chunk-vv3f8mb6.js +5314 -0
  80. package/dist/cli/chunk-vv3f8mb6.js.map +58 -0
  81. package/dist/cli/chunk-vxv4x1n8.js +17 -0
  82. package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
  83. package/dist/cli/chunk-wb067mv3.js +758 -0
  84. package/dist/cli/chunk-wb067mv3.js.map +13 -0
  85. package/dist/cli/chunk-wd27zjcz.js +60 -0
  86. package/dist/cli/chunk-wd27zjcz.js.map +10 -0
  87. package/dist/cli/chunk-wkq5tbtq.js +1141 -0
  88. package/dist/cli/chunk-wkq5tbtq.js.map +19 -0
  89. package/dist/cli/chunk-x1vrdjyk.js +1967 -0
  90. package/dist/cli/chunk-x1vrdjyk.js.map +34 -0
  91. package/dist/cli/chunk-x66c5yjn.js +23 -0
  92. package/dist/cli/chunk-x66c5yjn.js.map +10 -0
  93. package/dist/cli/index.js +55 -27587
  94. package/dist/cli/index.js.map +5 -243
  95. package/dist/types/ai/ask-context.d.ts +26 -0
  96. package/dist/types/core/code-fences.d.ts +11 -0
  97. package/dist/types/core/config-input.d.ts +10 -0
  98. package/dist/types/core/package-root.d.ts +1 -1
  99. package/dist/types/core/schema.d.ts +74 -1
  100. package/docs/02-deployment.mdx +1 -1
  101. package/docs/configuration/analytics.mdx +21 -2
  102. package/docs/configuration/ask-ai.mdx +1 -1
  103. package/docs/configuration/customization.mdx +2 -9
  104. package/docs/content/syntax.mdx +1 -1
  105. package/docs/reference/cli.mdx +1 -1
  106. package/package.json +16 -14
  107. package/src/ai/api/handlers.ts +4 -7
  108. package/src/ai/api/paths.ts +8 -0
  109. package/src/ai/api/spec.ts +2 -1
  110. package/src/ai/ask-context.ts +378 -22
  111. package/src/astro/generate.ts +25 -29
  112. package/src/astro/include-hmr.ts +10 -13
  113. package/src/astro/include-refresh.ts +0 -0
  114. package/src/astro/index.ts +6 -1
  115. package/src/astro/integration.ts +269 -53
  116. package/src/astro/module-types.ts +74 -0
  117. package/src/astro/templates.ts +85 -97
  118. package/src/audit/image-size.ts +10 -8
  119. package/src/cli/command-meta.ts +77 -0
  120. package/src/cli/commands/add.ts +2 -4
  121. package/src/cli/commands/audit.ts +2 -4
  122. package/src/cli/commands/build.ts +42 -346
  123. package/src/cli/commands/check.ts +2 -4
  124. package/src/cli/commands/dev.ts +31 -42
  125. package/src/cli/commands/doctor.ts +2 -4
  126. package/src/cli/commands/eject.ts +3 -41
  127. package/src/cli/commands/eval.ts +2 -5
  128. package/src/cli/commands/init.ts +2 -4
  129. package/src/cli/commands/mcp-stdio.ts +2 -5
  130. package/src/cli/commands/preview.ts +3 -5
  131. package/src/cli/commands/sync.ts +2 -4
  132. package/src/cli/commands/translate.ts +2 -5
  133. package/src/cli/commands/validate.ts +2 -4
  134. package/src/cli/commands/version.ts +2 -4
  135. package/src/cli/eject-scripts.ts +0 -45
  136. package/src/cli/host-args.ts +16 -0
  137. package/src/cli/index.ts +84 -35
  138. package/src/cli/lazy-command.ts +47 -0
  139. package/src/components/content/GithubInfo.astro +4 -1
  140. package/src/components/content/mermaid-element.ts +8 -0
  141. package/src/components/layout/Analytics.astro +20 -1
  142. package/src/components/layout/PageLayout.astro +14 -3
  143. package/src/components/layout/ReferenceLayout.astro +15 -4
  144. package/src/components/layout/RootLayout.astro +15 -4
  145. package/src/components/layout/analytics-client.ts +2 -1
  146. package/src/components/layout/page-locale.ts +29 -0
  147. package/src/components/openapi/AsyncApiOperation.astro +5 -3
  148. package/src/components/openapi/Authorization.astro +4 -6
  149. package/src/components/openapi/Bindings.astro +2 -2
  150. package/src/components/openapi/Description.astro +109 -0
  151. package/src/components/openapi/GraphqlFieldsTable.astro +5 -7
  152. package/src/components/openapi/GraphqlOperation.astro +4 -3
  153. package/src/components/openapi/GraphqlType.astro +4 -6
  154. package/src/components/openapi/ParametersTable.astro +5 -7
  155. package/src/components/openapi/RequestBody.astro +2 -4
  156. package/src/components/openapi/Responses.astro +4 -3
  157. package/src/components/openapi/SchemaProperty.astro +11 -6
  158. package/src/components/openapi/description.ts +91 -0
  159. package/src/core/api-name.ts +18 -0
  160. package/src/core/code-fences.ts +48 -0
  161. package/src/core/config-input.ts +10 -0
  162. package/src/core/content-assets.ts +3 -7
  163. package/src/core/includes.ts +3 -7
  164. package/src/core/package-root.ts +1 -1
  165. package/src/core/schema.ts +27 -0
  166. package/src/core/sources/normalize.ts +2 -37
  167. package/src/core/sources/obsidian.ts +3 -2
  168. package/src/core/svg-dimensions.ts +97 -0
  169. package/src/core/version-cut.ts +2 -2
  170. package/src/deploy/artifacts.ts +370 -0
  171. package/src/deploy/cloudflare-negotiation.ts +97 -32
  172. package/src/deploy/function-bundle.ts +66 -20
  173. package/src/deploy/sitemap.ts +6 -0
  174. package/src/deploy/vercel-negotiation.ts +8 -30
  175. package/src/og/card.ts +6 -12
  176. package/src/openapi/render-mdx.ts +9 -5
  177. package/src/registry/eject.ts +0 -2
  178. package/src/theme/entry.ts +9 -2
@@ -1,19 +1,17 @@
1
1
  import { existsSync } from "node:fs";
2
- import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { readdir, readFile, stat, writeFile } from "node:fs/promises";
3
3
 
4
4
  import { build } from "astro";
5
5
  import { defineCommand } from "citty";
6
- import { dirname, join, resolve } from "pathe";
6
+ import { join } from "pathe";
7
7
 
8
- import { buildAgentReadability } from "../../ai/agent-readability.ts";
9
8
  import {
10
9
  API_CATALOG_PATH,
11
10
  API_CATALOG_TYPE,
12
- buildApiCatalog,
13
11
  hasApiCatalog,
14
12
  } from "../../ai/api-catalog.ts";
13
+ import { pageJsonPath } from "../../ai/api/paths.ts";
15
14
  import { buildHomeLinkHeader } from "../../ai/link-headers.ts";
16
- import { buildLlmsFiles } from "../../ai/llms.ts";
17
15
  import {
18
16
  agentMarkdown,
19
17
  buildRawMarkdown,
@@ -21,16 +19,10 @@ import {
21
19
  markdownTokenCount,
22
20
  } from "../../ai/markdown.ts";
23
21
  import {
24
- AGENT_SKILLS_DIR,
25
- buildSkillsIndex,
26
- collectSkills,
27
- } from "../../ai/skills.ts";
28
- import type { SkillArtifact } from "../../ai/skills.ts";
29
- import {
30
- buildSignaturesDirectory,
31
22
  SIGNATURES_DIRECTORY_PATH,
32
23
  SIGNATURES_DIRECTORY_TYPE,
33
24
  } from "../../ai/web-bot-auth.ts";
25
+ import { publishBuildProject } from "../../astro/integration.ts";
34
26
  import { ensureGitignore } from "../../core/gitignore.ts";
35
27
  import type { BlumeProject } from "../../core/project-graph.ts";
36
28
  import type { ResolvedConfig } from "../../core/schema.ts";
@@ -39,7 +31,6 @@ import type { ProjectContext } from "../../core/types.ts";
39
31
  import {
40
32
  ADAPTER_IGNORE_DIRS,
41
33
  deployStaticDir,
42
- readsHeaderFiles,
43
34
  servesClientSubdir,
44
35
  surfaceAdapterOutput,
45
36
  } from "../../deploy/adapter-output.ts";
@@ -52,18 +43,9 @@ import {
52
43
  blumeDependencyNames,
53
44
  functionBundleVerdict,
54
45
  } from "../../deploy/function-bundle.ts";
55
- import { buildNetlifyHeaders } from "../../deploy/headers.ts";
56
- import {
57
- buildNetlifyRedirects,
58
- buildRedirectManifest,
59
- buildVercelConfig,
60
- platformRedirects,
61
- } from "../../deploy/redirects.ts";
62
- import { buildRobots } from "../../deploy/robots.ts";
63
- import { buildSitemapFiles } from "../../deploy/sitemap.ts";
46
+ import { platformRedirects } from "../../deploy/redirects.ts";
64
47
  import { injectNegotiationRoutes } from "../../deploy/vercel-negotiation.ts";
65
- import { buildSearchIndex } from "../../search/build.ts";
66
- import { syncSearchProvider } from "../../search/sync/index.ts";
48
+ import { commandMeta } from "../command-meta.ts";
67
49
  import { refuseIfDevRunning } from "../dev-lock.ts";
68
50
  import { logger } from "../log.ts";
69
51
  import { prepareProject } from "../prepare.ts";
@@ -102,208 +84,6 @@ const validateBudgetFlags = (args: BudgetArgs): void => {
102
84
  }
103
85
  };
104
86
 
105
- /**
106
- * Emit platform redirect files for a static build (adapters wire redirects
107
- * natively). Always writes the manifest; writes `_redirects`/`vercel.json` only
108
- * when the user hasn't shipped one via public/. Note that Vercel's
109
- * git-integration builds read `vercel.json` from the repository root only —
110
- * the copy emitted here takes effect when the dist folder itself is deployed
111
- * directly via the Vercel CLI.
112
- */
113
- const emitRedirectFiles = async (
114
- config: ResolvedConfig,
115
- distDir: string
116
- ): Promise<void> => {
117
- const redirects = platformRedirects(config);
118
- if (redirects.length === 0 || config.deployment.output !== "static") {
119
- return;
120
- }
121
- await writeFile(
122
- join(distDir, "blume-redirects.json"),
123
- buildRedirectManifest(redirects),
124
- "utf-8"
125
- );
126
- const platformFiles = [
127
- { content: buildNetlifyRedirects(redirects), name: "_redirects" },
128
- { content: buildVercelConfig(redirects), name: "vercel.json" },
129
- ];
130
- await Promise.all(
131
- platformFiles.map((file) =>
132
- existsSync(join(distDir, file.name))
133
- ? Promise.resolve()
134
- : writeFile(join(distDir, file.name), file.content, "utf-8")
135
- )
136
- );
137
- logger.success(`Emitted redirect files for ${redirects.length} redirect(s)`);
138
- };
139
-
140
- /**
141
- * Emit a `_headers` file so Netlify / Cloudflare serve the raw AI-ready
142
- * endpoints (`*.md`, `*.mdx`, `*.txt`) with an explicit `charset=utf-8`. Without
143
- * it those hosts send `text/markdown` / `text/plain` with no charset and
144
- * browsers fall back to Windows-1252, garbling any non-ASCII docs (#82).
145
- *
146
- * The same file carries the rest of the agent-discovery surface that only a
147
- * response header can express: the homepage `Link` header (RFC 8288, see
148
- * `ai/link-headers.ts`), and the registered media types for the extensionless
149
- * well-known files — `application/linkset+json` for the API catalog, the
150
- * signatures directory, and the Agent Skills archives. A static host serves
151
- * those as `octet-stream` or nothing at all without a rule.
152
- *
153
- * A `_headers` shipped in `public/` wins, exactly like `_redirects` — the opt-out
154
- * is checked at its source rather than in `dist`, because on Cloudflare the file
155
- * in `dist` is not necessarily the user's: `@astrojs/cloudflare` writes its own
156
- * `_headers` (an immutable `Cache-Control` rule for `/_astro/*`) during the
157
- * build, before this runs. Testing `dist` therefore read an adapter-generated
158
- * file as a user opt-out and skipped silently. When both exist, the adapter's
159
- * rules are preserved and ours are appended.
160
- *
161
- * Gated on {@link readsHeaderFiles}, not on `output === "static"`. A **Cloudflare
162
- * server** build serves `dist/client` through the Worker's ASSETS binding, and
163
- * Workers static assets honor `_headers` from that directory — so the file
164
- * applies there too, and skipping it left every Cloudflare server build with no
165
- * `Link` header and no media type on its own discovery files. The charset half
166
- * of this file *is* redundant on a server build, because the runtime endpoint
167
- * sets Content-Type on the Response itself; the `Link` and well-known halves are
168
- * not, and one conclusion about the first was applied to all three.
169
- *
170
- * Exported for the test suite, which exercises it in a subprocess like the
171
- * other command helpers.
172
- */
173
- export const emitHeaderFiles = async (
174
- project: BlumeProject,
175
- distDir: string
176
- ): Promise<void> => {
177
- const { config } = project;
178
- if (
179
- !readsHeaderFiles(config.deployment) ||
180
- existsSync(join(project.context.root, "public", "_headers"))
181
- ) {
182
- return;
183
- }
184
- const ours = buildNetlifyHeaders(
185
- config,
186
- buildHomeLinkHeader(config, markdownRoutePaths(project))
187
- );
188
- // An adapter may have written its own rules here already (Cloudflare adds an
189
- // immutable Cache-Control for /_astro/*). Keep them and append ours: both
190
- // sets are wanted, and `_headers` has no merge semantics beyond order.
191
- const target = join(distDir, "_headers");
192
- const existing = existsSync(target) ? await readFile(target, "utf-8") : "";
193
- await writeFile(
194
- target,
195
- existing ? `${existing.trimEnd()}\n${ours}` : ours,
196
- "utf-8"
197
- );
198
- logger.success(
199
- "Emitted _headers (UTF-8 Content-Type + homepage Link header)"
200
- );
201
- };
202
-
203
- /**
204
- * Collect the Agent Skills `ai.skills` publishes, once per build, so both the
205
- * skills surface and llms.txt (which lists them) read the same set. Empty
206
- * when the feature is off, the directory is missing, nothing in it is
207
- * publishable (each with a warning), or a user-shipped
208
- * `public/.well-known/agent-skills/index.json` already owns the surface.
209
- */
210
- const collectConfiguredSkills = async (
211
- project: BlumeProject,
212
- distDir: string
213
- ): Promise<SkillArtifact[]> => {
214
- const configured = project.config.ai.skills;
215
- if (!configured) {
216
- return [];
217
- }
218
- const dir = resolve(project.context.root, configured);
219
- if (!existsSync(dir)) {
220
- logger.warn(
221
- `ai.skills points at "${configured}" (${dir}), which does not exist; no skills published.`
222
- );
223
- return [];
224
- }
225
- if (existsSync(join(distDir, AGENT_SKILLS_DIR.slice(1), "index.json"))) {
226
- return [];
227
- }
228
- const { skills, warnings } = await collectSkills(dir);
229
- for (const warning of warnings) {
230
- logger.warn(warning);
231
- }
232
- if (skills.length === 0) {
233
- logger.warn(`ai.skills: no publishable skills found in "${configured}".`);
234
- }
235
- return skills;
236
- };
237
-
238
- /**
239
- * Publish the collected Agent Skills: copy each skill artifact under
240
- * `.well-known/agent-skills/` and emit the discovery index. A user-shipped
241
- * `public/.well-known/agent-skills/index.json` takes over the whole surface
242
- * (the collector returns nothing then), matching every other generated
243
- * artifact.
244
- */
245
- const emitAgentSkills = async (
246
- project: BlumeProject,
247
- distDir: string,
248
- skills: readonly SkillArtifact[]
249
- ): Promise<void> => {
250
- if (skills.length === 0) {
251
- return;
252
- }
253
- const outDir = join(distDir, AGENT_SKILLS_DIR.slice(1));
254
- await Promise.all(
255
- skills.map(async (skill) => {
256
- const target = join(outDir, skill.path);
257
- await mkdir(dirname(target), { recursive: true });
258
- await writeFile(target, skill.content);
259
- })
260
- );
261
- await writeFile(
262
- join(outDir, "index.json"),
263
- buildSkillsIndex(skills, project.config),
264
- "utf-8"
265
- );
266
- logger.success(
267
- `Published ${skills.length} agent skill(s) (.well-known/agent-skills/index.json)`
268
- );
269
- };
270
-
271
- /**
272
- * Emit the generated `.well-known` discovery files — the RFC 9727 API catalog
273
- * and the Web Bot Auth signature directory — each skipped when the feature is
274
- * off or when the user ships their own copy via public/ (already in dist by
275
- * the time this runs).
276
- */
277
- const emitWellKnownFiles = async (
278
- config: ResolvedConfig,
279
- distDir: string
280
- ): Promise<void> => {
281
- const files = [
282
- {
283
- content: buildSignaturesDirectory(config),
284
- label: "Web Bot Auth",
285
- path: SIGNATURES_DIRECTORY_PATH,
286
- },
287
- {
288
- content: buildApiCatalog(config),
289
- label: "RFC 9727",
290
- path: API_CATALOG_PATH,
291
- },
292
- ];
293
- for (const file of files) {
294
- const target = join(distDir, file.path.slice(1));
295
- if (!file.content || existsSync(target)) {
296
- continue;
297
- }
298
- // Sequential by nature: both files share the .well-known dir creation.
299
- // oxlint-disable-next-line no-await-in-loop
300
- await mkdir(join(distDir, ".well-known"), { recursive: true });
301
- // oxlint-disable-next-line no-await-in-loop
302
- await writeFile(target, file.content, "utf-8");
303
- logger.success(`Generated ${file.path.slice(1)} (${file.label})`);
304
- }
305
- };
306
-
307
87
  /**
308
88
  * Splice `Accept: text/markdown` negotiation routes into the Vercel adapter's
309
89
  * Build Output config, so a content-page request that prefers Markdown gets the
@@ -436,6 +216,15 @@ const emitCloudflareNegotiation = async (
436
216
  contentRoutePaths: project.manifest.routes.map((route) => route.path),
437
217
  homeLinkHeader: buildHomeLinkHeader(config, routePaths),
438
218
  homeTokens: home ? markdownTokenCount(agentMarkdown(home)) : undefined,
219
+ // Exactly the per-page JSON documents the API emits (see `pageParams`):
220
+ // the non-hidden routes with agent Markdown, when the API is on.
221
+ pageJsonPaths: config.ai.api
222
+ ? project.manifest.routes
223
+ .filter(
224
+ (route) => !route.hidden && rawMarkdown[route.path] !== undefined
225
+ )
226
+ .map((route) => pageJsonPath(route.path))
227
+ : [],
439
228
  // The wrapper Worker matches full served URLs, so the redirects are
440
229
  // based the same way the platform files are — it answers any the
441
230
  // worker-first rules claim, where `_redirects` is never consulted and
@@ -616,118 +405,22 @@ export const isolatedStaticDir = (
616
405
  };
617
406
 
618
407
  /**
619
- * Generate `llms.txt`/`llms-full.txt` into the dist dir. A user's own file in
620
- * `public/` (copied into dist by Astro before this runs, like the sitemap and
621
- * robots.txt) wins over the generated one — each file is checked and replaced
622
- * independently, so a custom `llms.txt` still gets a generated `llms-full.txt`.
408
+ * Print the build summary box and run the optional bundle report / budget
409
+ * gate against the served static dir. The deploy artifacts themselves
410
+ * (search index, llms.txt, sitemap, robots, redirect and header files, …)
411
+ * were written by the integration's `astro:build:done` hook during
412
+ * `build()` — see `deploy/artifacts.ts`. Exits non-zero if a budget is
413
+ * exceeded.
623
414
  */
624
- const publishLlmsFiles = async (
625
- project: BlumeProject,
626
- distDir: string,
627
- skills: readonly SkillArtifact[]
628
- ): Promise<void> => {
629
- const indexPath = join(distDir, "llms.txt");
630
- const fullPath = join(distDir, "llms-full.txt");
631
- const writeIndex = !existsSync(indexPath);
632
- const writeFull = !existsSync(fullPath);
633
- if (!(writeIndex || writeFull)) {
634
- return;
635
- }
636
- const { index, full } = await buildLlmsFiles(project, { skills });
637
- const writes: Promise<void>[] = [];
638
- if (writeIndex) {
639
- writes.push(writeFile(indexPath, index, "utf-8"));
640
- }
641
- if (writeFull) {
642
- writes.push(writeFile(fullPath, full, "utf-8"));
643
- }
644
- await Promise.all(writes);
645
- logger.success(
646
- `Generated ${[
647
- writeIndex ? "llms.txt" : null,
648
- writeFull ? "llms-full.txt" : null,
649
- ]
650
- .filter(Boolean)
651
- .join(" and ")}`
652
- );
653
- };
654
-
655
- /**
656
- * Run every deploy post-step of a real (non-isolated) build: the search index +
657
- * hosted-provider sync, llms.txt, sitemap/robots, redirect files, the summary
658
- * box, and the optional bundle report / budget gate. Exits non-zero if a budget
659
- * is exceeded. Isolated verify builds skip all of this except the bundle
660
- * report / budget gate, which they run against their own output.
661
- */
662
- const publishBuildArtifacts = async (
415
+ const reportBuild = async (
663
416
  project: BlumeProject,
664
417
  distDir: string,
665
418
  args: { analyze?: boolean } & BudgetArgs
666
419
  ): Promise<void> => {
667
- if (project.config.search.provider === "pagefind") {
668
- logger.start("Building search index");
669
- const indexed = await buildSearchIndex(distDir);
670
- logger.success(`Indexed ${indexed} page(s) for search`);
671
- }
672
-
673
- // Upload the index to a hosted provider (Algolia, Orama Cloud, Typesense).
674
- // Skipped with a warning when its admin key isn't configured.
675
- await syncSearchProvider(project, {
676
- start: (message) => logger.start(message),
677
- success: (message) => logger.success(message),
678
- warn: (message) => logger.warn(message),
679
- });
680
-
681
- // Collected once: llms.txt lists the skills the build publishes below.
682
- const skills = await collectConfiguredSkills(project, distDir);
683
- if (project.config.ai.llmsTxt.enabled) {
684
- await publishLlmsFiles(project, distDir, skills);
685
- }
686
-
687
- // A user's own public/ file (copied into dist by Astro) always wins.
688
- const sitemapFiles = buildSitemapFiles(project);
689
- if (sitemapFiles && !existsSync(join(distDir, "sitemap.xml"))) {
690
- await Promise.all(
691
- sitemapFiles.map((file) =>
692
- writeFile(join(distDir, file.name), file.xml, "utf-8")
693
- )
694
- );
695
- logger.success(
696
- sitemapFiles.length === 1
697
- ? "Generated sitemap.xml"
698
- : `Generated sitemap.xml (index of ${sitemapFiles.length - 1} sitemap files)`
699
- );
700
- }
701
-
702
- const robots = buildRobots(project);
703
- if (robots && !existsSync(join(distDir, "robots.txt"))) {
704
- await writeFile(join(distDir, "robots.txt"), robots, "utf-8");
705
- logger.success("Generated robots.txt");
706
- }
707
-
708
- const agentReadability = buildAgentReadability(project);
709
- if (
710
- agentReadability &&
711
- !existsSync(join(distDir, "agent-readability.json"))
712
- ) {
713
- await writeFile(
714
- join(distDir, "agent-readability.json"),
715
- `${JSON.stringify(agentReadability, null, 2)}\n`,
716
- "utf-8"
717
- );
718
- logger.success("Generated agent-readability.json");
719
- }
720
-
721
- await emitWellKnownFiles(project.config, distDir);
722
- await emitAgentSkills(project, distDir, skills);
723
-
724
- await emitRedirectFiles(project.config, distDir);
725
- await emitHeaderFiles(project, distDir);
726
-
727
420
  const { config } = project;
728
421
  const features = serverFeatures(config);
729
- // `buildSitemapFiles` returns null both when the sitemap is disabled and when no
730
- // `site` is configured — only the latter deserves the remediation hint.
422
+ // The sitemap needs both the flag and a `site` (absolute URLs) — only the
423
+ // latter deserves the remediation hint.
731
424
  const sitemapNote = config.seo.sitemap
732
425
  ? "no (set deployment.site)"
733
426
  : "no (seo.sitemap is false)";
@@ -738,9 +431,9 @@ const publishBuildArtifacts = async (
738
431
  `Site ${config.deployment.site ?? "not set"}`,
739
432
  `Search ${config.search.provider}`,
740
433
  `Redirects ${config.redirects.length}`,
741
- `Sitemap ${sitemapFiles ? "yes" : sitemapNote}`,
742
- `Robots ${robots ? "yes" : "no"}`,
743
- `Agent JSON ${agentReadability ? "yes" : "no"}`,
434
+ `Sitemap ${config.deployment.site && config.seo.sitemap ? "yes" : sitemapNote}`,
435
+ `Robots ${config.seo.robots ? "yes" : "no"}`,
436
+ `Agent JSON ${config.seo.agentReadability ? "yes" : "no"}`,
744
437
  `LLM files ${config.ai.llmsTxt.enabled ? "yes" : "no"}`,
745
438
  `Server features ${features.length > 0 ? features.join(", ") : "none"}`,
746
439
  ].join("\n")
@@ -800,10 +493,7 @@ export const buildCommand = defineCommand({
800
493
  type: "boolean",
801
494
  },
802
495
  },
803
- meta: {
804
- description: "Build the docs site for production.",
805
- name: "build",
806
- },
496
+ meta: commandMeta.build,
807
497
  async run({ args }) {
808
498
  const root = process.cwd();
809
499
 
@@ -852,18 +542,24 @@ export const buildCommand = defineCommand({
852
542
  `Building ${project.graph.pages.length} page(s) (${project.config.deployment.output} output)`
853
543
  );
854
544
 
545
+ // Hand the scanned project to the integration: its `astro:build:done`
546
+ // hook writes the deploy artifacts (search index, llms.txt, sitemap, …)
547
+ // into Astro's client output during the build. An isolated build is a
548
+ // throwaway verify that only needs to confirm the site compiles and
549
+ // renders, so it publishes nothing — no network post-steps (a hosted
550
+ // search sync would push), no deploy artifacts.
551
+ if (!runtimeDir) {
552
+ publishBuildProject(project);
553
+ }
554
+
855
555
  await build({
856
556
  logLevel: "info",
857
557
  root: project.context.outDir,
858
558
  });
859
559
 
860
- // An isolated build is a throwaway verify: it only needs to confirm the site
861
- // compiles and renders. Skip the network post-steps (search sync) and
862
- // deploy artifacts (index/llms/sitemap/robots/redirects) that only matter
863
- // for a real publish and would push to hosted providers. The bundle report
864
- // and budget gate still run, though — `blume build --isolated --budget-js
865
- // 100` exiting 0 without measuring anything would be a silent false pass
866
- // in CI.
560
+ // The bundle report and budget gate still run for an isolated build —
561
+ // `blume build --isolated --budget-js 100` exiting 0 without measuring
562
+ // anything would be a silent false pass in CI.
867
563
  if (runtimeDir) {
868
564
  if (
869
565
  project.config.deployment.output === "server" &&
@@ -918,7 +614,7 @@ export const buildCommand = defineCommand({
918
614
  await emitCloudflareNegotiation(project, markdownRoutePaths(project));
919
615
  }
920
616
 
921
- await publishBuildArtifacts(
617
+ await reportBuild(
922
618
  project,
923
619
  deployStaticDir(project.config, project.context),
924
620
  args
@@ -6,6 +6,7 @@ import { defineCommand } from "citty";
6
6
  import { join } from "pathe";
7
7
 
8
8
  import { ensureGitignore } from "../../core/gitignore.ts";
9
+ import { commandMeta } from "../command-meta.ts";
9
10
  import { refuseIfDevRunning } from "../dev-lock.ts";
10
11
  import { logger } from "../log.ts";
11
12
  import { prepareProject } from "../prepare.ts";
@@ -26,10 +27,7 @@ export const checkCommand = defineCommand({
26
27
  type: "boolean",
27
28
  },
28
29
  },
29
- meta: {
30
- description: "Type-check the docs site with astro check.",
31
- name: "check",
32
- },
30
+ meta: commandMeta.check,
33
31
  async run({ args }) {
34
32
  const root = process.cwd();
35
33
 
@@ -4,37 +4,29 @@ import { defineCommand } from "citty";
4
4
  import { debounce } from "perfect-debounce";
5
5
 
6
6
  import { generateRuntime } from "../../astro/generate.ts";
7
- import { showBlumeErrorOverlay } from "../../astro/integration.ts";
7
+ import {
8
+ refreshBlumeContent,
9
+ showBlumeErrorOverlay,
10
+ } from "../../astro/integration.ts";
8
11
  import { scanProject } from "../../core/project-graph.ts";
9
12
  import { resolveRuntimeDir } from "../../core/project.ts";
10
13
  import { parsePort } from "../args.ts";
14
+ import { commandMeta } from "../command-meta.ts";
11
15
  import {
12
16
  acquireDevLock,
13
17
  describeDevLock,
14
18
  DevLockHeldError,
15
19
  updateDevLockPort,
16
20
  } from "../dev-lock.ts";
21
+ import { normalizeHost } from "../host-args.ts";
17
22
  import { logger, reportDiagnostics } from "../log.ts";
18
23
  import { prepareProject } from "../prepare.ts";
19
24
 
20
- /**
21
- * Resolve a `--host` flag value into what Astro/Vite's `server.host` expects.
22
- * citty has no mixed string/boolean arg type, so `host` is declared as a
23
- * string and a bare `--host` parses as `""` (the CLI entry rewrites it to
24
- * `--host=` first; see `host-args.ts`) — Node would bind all interfaces
25
- * for `""`, but Vite's `resolveHostname` treats it as a literal hostname and
26
- * prints malformed URLs like `http://:4321/`. Match Astro's own `--host`
27
- * semantics instead: bare flag → `true` (bind all interfaces), `--host
28
- * 10.0.0.1` → that address, absent → `false` (localhost only).
29
- */
30
- export const normalizeHost = (host: string | undefined): boolean | string =>
31
- host === "" ? true : (host ?? false);
32
-
33
25
  /**
34
26
  * A fingerprint of the route set: the sorted `path entryId` pairs. It changes
35
27
  * when a page is added, removed, or renamed (a folder rename shifts many at
36
28
  * once) but stays equal across pure body edits — so the dev loop can tell a
37
- * "structural" change (needs a cold restart) from a hot-reloadable one.
29
+ * "structural" change (needs a content re-sync) from a hot-reloadable one.
38
30
  */
39
31
  const routeSignature = (
40
32
  routes: readonly { entryId: string; path: string }[]
@@ -63,10 +55,7 @@ export const devCommand = defineCommand({
63
55
  },
64
56
  strict: { description: "Fail on diagnostics.", type: "boolean" },
65
57
  },
66
- meta: {
67
- description: "Start the Blume development server.",
68
- name: "dev",
69
- },
58
+ meta: commandMeta.dev,
70
59
  async run({ args }) {
71
60
  const root = process.cwd();
72
61
  const preview = args.preview ?? false;
@@ -110,10 +99,10 @@ export const devCommand = defineCommand({
110
99
  strict: args.strict,
111
100
  });
112
101
 
113
- // A factory so `runRegenerate` can recreate the server on a structural
114
- // (route-set) change: only a cold container re-globs Astro's content store,
115
- // which its in-place config restart doesn't. `open` is honored on first
116
- // boot only — a restart must not reopen the browser.
102
+ // A factory so the regenerate loop can recreate the server when a
103
+ // structural (route-set) change can't be re-synced in place (see below).
104
+ // `open` is honored on first boot only — a restart must not reopen the
105
+ // browser.
117
106
  const createServer = (listenPort: number | undefined, open: boolean) =>
118
107
  dev({
119
108
  logLevel: args.debug ? "debug" : "info",
@@ -141,14 +130,16 @@ export const devCommand = defineCommand({
141
130
  let lastSignature = routeSignature(project.manifest.routes);
142
131
 
143
132
  // Watch user inputs and regenerate the runtime data on change. A body edit
144
- // hot-reloads via Vite (fast path). A route-set change instead forces a cold
145
- // server restart: Astro's in-place content sync never re-globs on a Blume
146
- // route change (it strips `integrations` from its cache digest) and its glob
147
- // watcher misses directory renames, so a renamed page 404s (`getEntry` reads
148
- // a stale in-memory store) until the server is restarted. We restart it
149
- // ourselves — stop, regenerate while down (no watcher races), then bring up
150
- // a fresh container whose cold sync re-globs everything. perfect-debounce
151
- // both debounces the watch burst (80ms) and single-flights the scan: a
133
+ // hot-reloads via Vite (fast path). A route-set change also needs Astro's
134
+ // content store re-synced: its glob watcher misses directory renames, so a
135
+ // renamed page would 404 (`getEntry` reads a stale store). Astro hands the
136
+ // integration `refreshContent` for exactly that — a full loader run
137
+ // against the live server, after which Astro's own store watcher clears
138
+ // the route cache and reloads the browser. Only a server that registered
139
+ // no refresh (none since Astro 5) falls back to a cold restart: stop,
140
+ // then bring up a fresh container whose cold sync re-globs everything.
141
+ // perfect-debounce both debounces the watch burst (80ms) and
142
+ // single-flights the scan: a
152
143
  // trigger during a run never starts a second run, only marks one trailing
153
144
  // rerun after the current settles. Both halves are load-bearing — a plain
154
145
  // debounce once let bursts stack overlapping scans until the heap was
@@ -166,23 +157,21 @@ export const devCommand = defineCommand({
166
157
  });
167
158
  const nextSignature = routeSignature(next.manifest.routes);
168
159
  const structural = nextSignature !== lastSignature;
169
- if (structural) {
160
+ // Generate first: the new runtime data (and any staged remote content)
161
+ // is on disk and published before the store re-syncs against it.
162
+ await generateRuntime(next);
163
+ if (structural && !(await refreshBlumeContent())) {
170
164
  await server.stop();
171
- await generateRuntime(next);
172
165
  server = await createServer(boundPort, false);
173
- } else {
174
- await generateRuntime(next);
175
166
  }
176
167
  // Commit the signature only after the (re)generation succeeded. If the
177
- // restart above throws mid-sequence, the signature stays stale so the
178
- // next watch event retries the structural path — committing early would
179
- // route it to the non-structural branch with the server still down.
168
+ // re-sync or restart above throws mid-sequence, the signature stays
169
+ // stale so the next watch event retries the structural path.
180
170
  lastSignature = nextSignature;
181
171
  // Surface any content/config errors in the terminal AND the browser
182
- // overlay. The terminal report must not be skipped: on a published
183
- // install the CLI bundle holds its own copy of the integration module,
184
- // separate from the Vite module graph that registers the overlay, so
185
- // the overlay call below can be a no-op there.
172
+ // overlay. The terminal report is not redundant: the overlay only
173
+ // shows once the browser has connected, and it clears on the next HMR
174
+ // update.
186
175
  reportDiagnostics(next.diagnostics, root);
187
176
  showBlumeErrorOverlay(next.diagnostics);
188
177
  } catch (error) {
@@ -9,6 +9,7 @@ import { packageRoot } from "../../core/package-root.ts";
9
9
  import { scanProject } from "../../core/project-graph.ts";
10
10
  import { serverFeatures } from "../../core/server-features.ts";
11
11
  import type { Diagnostic } from "../../core/types.ts";
12
+ import { commandMeta } from "../command-meta.ts";
12
13
  import { reportInternalError } from "../internal-error.ts";
13
14
  import {
14
15
  flushStdout,
@@ -41,10 +42,7 @@ export const doctorCommand = defineCommand({
41
42
  type: "boolean",
42
43
  },
43
44
  },
44
- meta: {
45
- description: "Diagnose common configuration and content problems.",
46
- name: "doctor",
47
- },
45
+ meta: commandMeta.doctor,
48
46
  async run({ args }) {
49
47
  const root = process.cwd();
50
48
  const diagnostics: Diagnostic[] = [];