blume 1.0.3 → 1.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 (126) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli/index.js +13784 -10579
  3. package/dist/cli/index.js.map +93 -61
  4. package/dist/types/core/config-input.d.ts +87 -8
  5. package/dist/types/core/data.d.ts +21 -0
  6. package/dist/types/core/deployment-env.d.ts +6 -0
  7. package/dist/types/core/diagnostics.d.ts +23 -0
  8. package/dist/types/core/i18n-ui.d.ts +140 -140
  9. package/dist/types/core/schema.d.ts +549 -370
  10. package/dist/types/core/sources/types.d.ts +3 -1
  11. package/dist/types/core/standard-schema.d.ts +41 -0
  12. package/dist/types/core/types.d.ts +23 -0
  13. package/dist/types/og/card.d.ts +63 -0
  14. package/dist/types/og/dimensions.d.ts +12 -0
  15. package/dist/types/openapi/references.d.ts +12 -7
  16. package/docs/01-quickstart.mdx +1 -1
  17. package/docs/02-deployment.mdx +9 -1
  18. package/docs/advanced/api-reference.mdx +22 -3
  19. package/docs/advanced/changelog.mdx +1 -1
  20. package/docs/advanced/skills.mdx +1 -1
  21. package/docs/configuration/ai.mdx +1 -1
  22. package/docs/configuration/customization.mdx +1 -1
  23. package/docs/configuration/export.mdx +1 -1
  24. package/docs/configuration/index.mdx +21 -1
  25. package/docs/configuration/search.mdx +28 -1
  26. package/docs/configuration/seo.mdx +40 -2
  27. package/docs/configuration/theming.mdx +1 -1
  28. package/docs/content/components.mdx +15 -2
  29. package/docs/content/index.mdx +1 -1
  30. package/docs/content/meta.mdx +1 -1
  31. package/docs/content/navigation.mdx +11 -1
  32. package/docs/content/sources.mdx +1 -1
  33. package/docs/content/syntax.mdx +116 -4
  34. package/docs/reference/cli.mdx +79 -1
  35. package/docs/reference/frontmatter.mdx +29 -1
  36. package/package.json +3 -3
  37. package/skills/blume-migrate/SKILL.md +170 -0
  38. package/skills/blume-migrate/assets/oxfmt@0.55.0.patch +20 -0
  39. package/skills/blume-migrate/references/docusaurus.md +95 -0
  40. package/skills/blume-migrate/references/fumadocs.md +95 -0
  41. package/skills/blume-migrate/references/mintlify.md +156 -0
  42. package/skills/blume-migrate/references/monorepo.md +224 -0
  43. package/skills/blume-migrate/references/nextra.md +76 -0
  44. package/skills/blume-migrate/references/starlight.md +116 -0
  45. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +478 -0
  46. package/src/ai/llms.ts +15 -0
  47. package/src/astro/adapter-root.ts +70 -0
  48. package/src/astro/component-slots.ts +3 -2
  49. package/src/astro/generate.ts +132 -42
  50. package/src/astro/index.ts +1 -0
  51. package/src/astro/pages.ts +18 -3
  52. package/src/astro/templates.ts +158 -56
  53. package/src/audit/agent.ts +114 -0
  54. package/src/audit/catalog.ts +826 -0
  55. package/src/audit/checks/assets.ts +177 -0
  56. package/src/audit/checks/content.ts +231 -0
  57. package/src/audit/checks/duplicates.ts +131 -0
  58. package/src/audit/checks/i18n.ts +246 -0
  59. package/src/audit/checks/indexability.ts +213 -0
  60. package/src/audit/checks/links.ts +223 -0
  61. package/src/audit/checks/llms.ts +135 -0
  62. package/src/audit/checks/network.ts +272 -0
  63. package/src/audit/checks/og-image.ts +113 -0
  64. package/src/audit/checks/redirects.ts +87 -0
  65. package/src/audit/checks/robots.ts +114 -0
  66. package/src/audit/checks/sitemap.ts +229 -0
  67. package/src/audit/checks/social.ts +238 -0
  68. package/src/audit/crawl.ts +259 -0
  69. package/src/audit/graph.ts +74 -0
  70. package/src/audit/html.ts +54 -0
  71. package/src/audit/image-size.ts +63 -0
  72. package/src/audit/locate.ts +33 -0
  73. package/src/audit/redirects.ts +74 -0
  74. package/src/audit/report.ts +278 -0
  75. package/src/audit/run.ts +198 -0
  76. package/src/audit/snapshot.ts +189 -0
  77. package/src/audit/types.ts +214 -0
  78. package/src/audit/url.ts +103 -0
  79. package/src/cli/commands/audit.ts +205 -0
  80. package/src/cli/commands/build.ts +51 -12
  81. package/src/cli/index.ts +2 -0
  82. package/src/components/content/Callout.astro +8 -2
  83. package/src/components/content/Prompt.astro +25 -13
  84. package/src/components/content/Tabs.astro +98 -15
  85. package/src/components/layout/Breadcrumbs.astro +1 -1
  86. package/src/components/layout/Header.astro +5 -8
  87. package/src/components/layout/Logo.astro +13 -1
  88. package/src/components/layout/PageFeedback.astro +2 -2
  89. package/src/components/layout/PageLayout.astro +9 -9
  90. package/src/components/layout/Pagination.astro +7 -7
  91. package/src/components/layout/RootLayout.astro +9 -11
  92. package/src/components/layout/Search.astro +36 -7
  93. package/src/components/layout/TableOfContents.astro +1 -1
  94. package/src/components/layout/nav-utils.ts +9 -7
  95. package/src/components/openapi/Authorization.astro +80 -0
  96. package/src/components/openapi/Operation.astro +19 -1
  97. package/src/components/openapi/ParametersTable.astro +1 -1
  98. package/src/components/openapi/security.ts +201 -0
  99. package/src/components/openapi/snippets.ts +42 -13
  100. package/src/core/config-input.ts +94 -8
  101. package/src/core/data.ts +18 -2
  102. package/src/core/deployment-env.ts +9 -0
  103. package/src/core/diagnostics.ts +59 -12
  104. package/src/core/links.ts +2 -91
  105. package/src/core/nav-diagnostics.ts +48 -4
  106. package/src/core/navigation.ts +55 -13
  107. package/src/core/probe.ts +136 -0
  108. package/src/core/project-graph.ts +8 -0
  109. package/src/core/schema.ts +100 -1
  110. package/src/core/sources/normalize.ts +198 -25
  111. package/src/core/sources/types.ts +3 -1
  112. package/src/core/sources/watch.ts +5 -0
  113. package/src/core/standard-schema.ts +54 -0
  114. package/src/core/types.ts +23 -0
  115. package/src/deploy/adapter-output.ts +27 -15
  116. package/src/deploy/headers.ts +66 -0
  117. package/src/deploy/redirects.ts +49 -9
  118. package/src/markdown/index.ts +2 -0
  119. package/src/markdown/language-icon.ts +2 -1
  120. package/src/markdown/table-wrap.ts +43 -0
  121. package/src/og/card.ts +128 -36
  122. package/src/og/index.ts +1 -1
  123. package/src/og/logo.ts +21 -0
  124. package/src/openapi/references.ts +19 -16
  125. package/src/search/popular.ts +33 -0
  126. package/src/theme/entry.ts +56 -6
@@ -10,7 +10,7 @@ import type { ResolvedConfig } from "../core/schema.ts";
10
10
  import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
11
11
  import { trimChar } from "../core/trim.ts";
12
12
  import type { ProjectContext } from "../core/types.ts";
13
- import { applyBaseToRedirects } from "../deploy/redirects.ts";
13
+ import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
14
14
  import { hasScalarReferences } from "../openapi/references.ts";
15
15
  import { searchProviderMeta } from "../search/providers.ts";
16
16
  import { buildFontEntries } from "../theme/fonts.ts";
@@ -80,6 +80,30 @@ const ADAPTER_OPTIONS: Record<string, string> = {
80
80
  node: '{ mode: "standalone" }',
81
81
  };
82
82
 
83
+ const WRANGLER_CONFIG_FILES = [
84
+ "wrangler.jsonc",
85
+ "wrangler.json",
86
+ "wrangler.toml",
87
+ ];
88
+
89
+ const resolveCloudflareAdapterArgs = (context: ProjectContext): string => {
90
+ const args: string[] = ['prerenderEnvironment: "node"'];
91
+ const wranglerPath = WRANGLER_CONFIG_FILES.map((file) =>
92
+ join(context.root, file)
93
+ ).find((file) => existsSync(file));
94
+ if (wranglerPath) {
95
+ let configPath = relative(context.outDir, wranglerPath);
96
+ // The wrangler config always lives at the project root, above the `.blume`
97
+ // runtime, so `relative` yields a `../…` path; normalize the theoretical
98
+ // sibling case to an explicit `./` so it reads as a relative import.
99
+ if (!configPath.startsWith(".") && !configPath.startsWith("/")) {
100
+ configPath = `./${configPath}`;
101
+ }
102
+ args.push(`configPath: ${JSON.stringify(configPath)}`);
103
+ }
104
+ return `{ ${args.join(", ")} }`;
105
+ };
106
+
83
107
  /**
84
108
  * Integration packages the generated runtime imports. Declaring them in
85
109
  * `.blume/package.json` lets Astro's framework-package crawl discover and bundle
@@ -142,11 +166,16 @@ export const runtimeDependencies = (options: {
142
166
  * static-prerender Vite environments.
143
167
  *
144
168
  * Two reasons a dep lands here:
145
- * - `@takumi-rs/core` (OG image rendering) is a native NAPI addon that loads a
146
- * platform-specific `.node` binding via `createRequire(import.meta.url)`.
147
- * Bundling it relocates `import.meta.url` and breaks the binding lookup
148
- * ("Cannot find native binding") on other platforms (e.g. the Linux CI
149
- * runner), so it must resolve from `node_modules` at runtime instead.
169
+ * - `takumi-js` (OG image rendering) loads `@takumi-rs/core`, a native NAPI
170
+ * addon that finds its platform-specific `.node` binding via
171
+ * `createRequire(import.meta.url)`. Bundling it relocates `import.meta.url`
172
+ * and breaks the binding lookup ("Cannot find native binding") on other
173
+ * platforms (e.g. the Linux CI runner), so it must resolve from
174
+ * `node_modules` at runtime instead. The prerender env matches these by
175
+ * exact specifier, so every entry point Blume imports has to be listed:
176
+ * the bare `takumi-js` (render) plus `takumi-js/helpers` (the `googleFonts`
177
+ * OG-font loader). The `@takumi-rs/*` packages are listed too so the native
178
+ * backend is never pulled into a chunk down any transitive path.
150
179
  * - The rest are pure-JS packages kept external so an isolated linker (Bun's
151
180
  * `isolated` mode, pnpm) doesn't bundle their symlinked store copies. When
152
181
  * Vite bundles such a package but leaves its own `node_modules` child
@@ -166,10 +195,13 @@ const RENDER_EXTERNAL_DEPS = [
166
195
  "@shikijs/transformers",
167
196
  "@takumi-rs/core",
168
197
  "@takumi-rs/helpers",
198
+ "@takumi-rs/wasm",
169
199
  "github-slugger",
170
200
  "katex",
171
201
  "shiki",
172
202
  "simple-icons",
203
+ "takumi-js",
204
+ "takumi-js/helpers",
173
205
  "zod",
174
206
  ];
175
207
 
@@ -188,6 +220,21 @@ const renderUserAliases = (
188
220
  const astroOutDir = (context: ProjectContext): string =>
189
221
  context.distDir ?? `${context.root}/dist`;
190
222
 
223
+ /**
224
+ * The root a deploy adapter is shown, in place of the `.blume` runtime Astro
225
+ * actually roots at. Adapters assume `outDir` is `<root>/dist` and resolve their
226
+ * own output (and Vercel's dependency trace) against `root`, so the root implied
227
+ * by Blume's `outDir` is the one that keeps that assumption true. See
228
+ * {@link withAdapterRoot}.
229
+ *
230
+ * For a normal build that is the project root (`<project>/dist` -> `<project>`).
231
+ * For a relocated runtime (`blume build --isolated`) it is the runtime dir
232
+ * itself (`<runtime>/dist` -> `<runtime>`), keeping a verify build's adapter
233
+ * output self-contained instead of overwriting the real `.vercel/output`.
234
+ */
235
+ const adapterRoot = (context: ProjectContext): string =>
236
+ dirname(astroOutDir(context));
237
+
191
238
  /**
192
239
  * Excludes Vite's pre-bundled dep cache from @vitejs/plugin-react. Astro's
193
240
  * react() replaces the plugin's default `/node_modules/` exclude with just
@@ -263,12 +310,26 @@ export const astroConfigTemplate = (options: {
263
310
  server && deployment.adapter
264
311
  ? `import adapter from "${ADAPTER_IMPORTS[deployment.adapter]}";\n`
265
312
  : "";
266
- const adapterArgs =
267
- server && deployment.adapter
268
- ? (ADAPTER_OPTIONS[deployment.adapter] ?? "")
269
- : "";
313
+ const adapterArgs = (() => {
314
+ if (!server || !deployment.adapter) {
315
+ return "";
316
+ }
317
+ if (deployment.adapter === "cloudflare") {
318
+ return resolveCloudflareAdapterArgs(context);
319
+ }
320
+ return ADAPTER_OPTIONS[deployment.adapter] ?? "";
321
+ })();
322
+ // Vercel resolves its Build Output tree and its `@vercel/nft` dependency
323
+ // trace against the Astro root, which for Blume is the hidden `.blume`
324
+ // runtime — leaving the traced function without its chunks or node_modules.
325
+ // The other adapters emit into `outDir` (cloudflare, node) or are surfaced
326
+ // afterwards (netlify), so none of them read `root` this way.
327
+ const adapterExpr =
328
+ deployment.adapter === "vercel"
329
+ ? `withAdapterRoot(adapter(${adapterArgs}), ${JSON.stringify(adapterRoot(context))})`
330
+ : `adapter(${adapterArgs})`;
270
331
  const adapterOption =
271
- server && deployment.adapter ? `\n adapter: adapter(${adapterArgs}),` : "";
332
+ server && deployment.adapter ? `\n adapter: ${adapterExpr},` : "";
272
333
 
273
334
  const siteOption = deployment.site
274
335
  ? `\n site: ${JSON.stringify(deployment.site)},`
@@ -291,10 +352,12 @@ export const astroConfigTemplate = (options: {
291
352
  : "";
292
353
 
293
354
  // Base the redirect paths the same way routes are based, so a redirect lands
294
- // under `basePath` too. Astro layers its own `base` (deployment.base) on top.
295
- const basedRedirects = applyBaseToRedirects(
355
+ // under `basePath` too. Astro layers its own `base` (deployment.base) onto
356
+ // `from` when matching, but never onto `to` — see applyBaseToAstroRedirects.
357
+ const basedRedirects = applyBaseToAstroRedirects(
296
358
  config.redirects,
297
- config.basePath
359
+ config.basePath,
360
+ deployment.base ?? ""
298
361
  );
299
362
  const redirectsOption =
300
363
  basedRedirects.length > 0
@@ -336,7 +399,13 @@ export const astroConfigTemplate = (options: {
336
399
  const svelteImport = needsSvelte
337
400
  ? `import svelte from "@astrojs/svelte";\n`
338
401
  : "";
339
- const blumeImport = `import { blumeIntegration, prerenderDepsPlugin, serverAppResolvePlugin } from "blume/astro";\n`;
402
+ const blumeImports = [
403
+ "blumeIntegration",
404
+ "prerenderDepsPlugin",
405
+ "serverAppResolvePlugin",
406
+ ...(adapterOption.includes("withAdapterRoot") ? ["withAdapterRoot"] : []),
407
+ ];
408
+ const blumeImport = `import { ${blumeImports.join(", ")} } from "blume/astro";\n`;
340
409
 
341
410
  // Twoslash runs first, before the always-on transformers, but only on fences
342
411
  // with the `twoslash` meta (explicitTrigger) — so it's opt-in per block with
@@ -410,6 +479,18 @@ export default defineConfig({
410
479
  devToolbar: { enabled: false },
411
480
  vite: {
412
481
  plugins: [tailwindcss(), prerenderDepsPlugin(), serverAppResolvePlugin()],
482
+ // Mermaid (lazy-loaded client-side for diagrams) statically imports dayjs as
483
+ // CJS (\`dayjs/dayjs.min.js\`). In dev, an un-pre-bundled dependency is served
484
+ // as raw ESM, and that UMD file exposes no \`default\` export, so mermaid
485
+ // throws on load and diagrams render blank. Forcing mermaid through the dep
486
+ // optimizer bundles dayjs with correct CJS interop. In a standalone install
487
+ // Blume's dynamic \`import("mermaid")\` lives inside \`node_modules/blume\`,
488
+ // which Vite's optimizer scan doesn't crawl, so mermaid is never discovered
489
+ // on its own — hence the explicit include. mermaid resolves through the
490
+ // \`blume\` package (it isn't a direct dep of the generated project), so the
491
+ // nested \`blume > mermaid\` form is required. Production (Rollup) already
492
+ // handles the interop, so this only affects dev.
493
+ optimizeDeps: { include: ["blume > mermaid"] },
413
494
  // Blume's render-time deps are forced external on both build environments so
414
495
  // native bindings resolve at runtime and isolated linkers don't bundle
415
496
  // symlinked store copies (which would surface their children as unresolvable
@@ -909,10 +990,11 @@ export function getStaticPaths() {
909
990
  }));
910
991
  }
911
992
 
912
- export function GET({ props }) {
913
- const entry = raw[props.route];
993
+ export function GET({ props }: { props: { route: string } }) {
994
+ const entries = raw as Record<string, { md?: string; mdx?: string }>;
995
+ const entry = entries[props.route];
914
996
  return new Response(entry ? ${
915
- kind === "md" ? "(entry.md ?? entry.mdx)" : "entry.mdx"
997
+ kind === "md" ? '(entry.md ?? entry.mdx ?? "")' : '(entry.mdx ?? "")'
916
998
  } : "", {
917
999
  headers: { "Content-Type": "text/markdown; charset=utf-8" },
918
1000
  });
@@ -976,8 +1058,9 @@ export function getStaticPaths() {
976
1058
  }));
977
1059
  }
978
1060
 
979
- export function GET({ props }) {
980
- return new Response(feeds[props.section] ?? "", {
1061
+ export function GET({ props }: { props: { section: string } }) {
1062
+ const bySection = feeds as Record<string, string>;
1063
+ return new Response(bySection[props.section] ?? "", {
981
1064
  headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
982
1065
  });
983
1066
  }
@@ -989,7 +1072,7 @@ export const ogEndpointTemplate = (
989
1072
  ): string =>
990
1073
  `// Generated by Blume. Do not edit.
991
1074
  import { renderOgImage } from "blume/og";
992
- import data from "../../generated/data.json";
1075
+ import data from "blume:data";
993
1076
 
994
1077
  export const prerender = true;
995
1078
 
@@ -997,9 +1080,9 @@ export const prerender = true;
997
1080
  const customRoutes = ${JSON.stringify(customRoutes)};
998
1081
 
999
1082
  export function getStaticPaths() {
1000
- const seen = new Set();
1001
- const paths = [];
1002
- const add = (slug, title) => {
1083
+ const seen = new Set<string>();
1084
+ const paths: { params: { slug: string }; props: { title: string } }[] = [];
1085
+ const add = (slug: string, title: string) => {
1003
1086
  if (seen.has(slug)) {
1004
1087
  return;
1005
1088
  }
@@ -1032,17 +1115,19 @@ const siteHost = (() => {
1032
1115
  }
1033
1116
  })();
1034
1117
 
1035
- export async function GET({ props }) {
1118
+ export async function GET({ props }: { props: { title: string } }) {
1036
1119
  const png = await renderOgImage({
1037
- accent: data.config.theme.accent.light,
1120
+ accent: data.config.og.palette?.accent ?? data.config.theme.accent.light,
1038
1121
  brand: data.config.title,
1039
1122
  description: data.config.description,
1040
- logo: data.config.logo?.svg,
1123
+ fonts: data.config.og.fonts,
1124
+ logo: data.config.og.logo,
1125
+ palette: data.config.og.palette,
1041
1126
  repo: repoSlug,
1042
1127
  site: siteHost,
1043
1128
  title: props.title,
1044
1129
  });
1045
- return new Response(png, {
1130
+ return new Response(new Uint8Array(png), {
1046
1131
  headers: {
1047
1132
  "Cache-Control": "public, max-age=31536000, immutable",
1048
1133
  "Content-Type": "image/png",
@@ -1127,6 +1212,7 @@ export const catchAllPageTemplate = (options: {
1127
1212
  return `---
1128
1213
  // Generated by Blume. Do not edit.
1129
1214
  import { getEntry, render } from "astro:content";
1215
+ import type { CollectionKey } from "astro:content";
1130
1216
  import RootLayout from "blume/components/layout/RootLayout.astro";
1131
1217
  import { withBase } from "blume/components/islands/base-path.ts";
1132
1218
  import { resolveSlot } from "blume/components/layout/overrides.ts";
@@ -1170,7 +1256,7 @@ import ApiTagOperations from "blume/components/openapi/ApiTagOperations.astro";
1170
1256
  import Operation from "blume/components/openapi/Operation.astro";
1171
1257
  ${mathImport}import { mdxComponents as userMdx, layoutOverrides } from "../generated/components.ts";
1172
1258
  import { islandComponents } from "../generated/islands.ts";
1173
- import data from "../generated/data.json";
1259
+ import data from "blume:data";
1174
1260
 
1175
1261
  const Color = Object.assign(ColorRoot, { Item: ColorItem, Row: ColorRow });
1176
1262
  const Tree = Object.assign(TreeRoot, { File: TreeFile, Folder: TreeFolder });
@@ -1239,7 +1325,7 @@ export function getStaticPaths() {
1239
1325
  }
1240
1326
 
1241
1327
  const { entryId, collection, route, title, indexable, editUrl, lastModified, locale, alternates, fallback } = Astro.props;
1242
- const entry = await getEntry(collection, entryId);
1328
+ const entry = await getEntry(collection as CollectionKey, entryId);
1243
1329
  if (!entry) {
1244
1330
  return new Response(null, { status: 404 });
1245
1331
  }
@@ -1274,18 +1360,18 @@ const canonical =
1274
1360
  // Locale resolution. With i18n on, pick the active locale's nav + dictionary,
1275
1361
  // build hreflang alternates, and derive the language-switcher targets.
1276
1362
  const i18n = data.config.i18n;
1277
- const localePrefix = (codeArg) =>
1363
+ const localePrefix = (codeArg: string) =>
1278
1364
  i18n && codeArg === i18n.defaultLocale && i18n.hideDefaultLocalePrefix
1279
1365
  ? ""
1280
1366
  : \`/\${codeArg}\`;
1281
- const localizeRoute = (logical, codeArg) => {
1367
+ const localizeRoute = (logical: string, codeArg: string) => {
1282
1368
  const prefix = localePrefix(codeArg);
1283
1369
  if (!prefix) {
1284
1370
  return logical;
1285
1371
  }
1286
1372
  return logical === "/" ? prefix : \`\${prefix}\${logical}\`;
1287
1373
  };
1288
- const stripLocale = (path, codeArg) => {
1374
+ const stripLocale = (path: string, codeArg: string) => {
1289
1375
  const prefix = localePrefix(codeArg);
1290
1376
  return prefix && path.startsWith(prefix) ? path.slice(prefix.length) || "/" : path;
1291
1377
  };
@@ -1302,7 +1388,7 @@ const contentLocale =
1302
1388
  const contentDir = i18n
1303
1389
  ? (i18n.locales.find((l) => l.code === contentLocale)?.dir ?? "ltr")
1304
1390
  : "ltr";
1305
- const absolute = (path) => {
1391
+ const absolute = (path: string) => {
1306
1392
  const p = withBase(path);
1307
1393
  return base + (p === "/" ? "" : p);
1308
1394
  };
@@ -1414,14 +1500,15 @@ import Update from "blume/components/content/Update.astro";
1414
1500
  import { withBase } from "blume/components/islands/base-path.ts";
1415
1501
  import { resolveSlot } from "blume/components/layout/overrides.ts";
1416
1502
  import { layoutOverrides } from "../generated/components.ts";
1417
- import data from "../generated/data.json";
1503
+ import data from "blume:data";
1418
1504
 
1419
1505
  export const prerender = true;
1420
1506
 
1421
- const entryDate = (entry) =>
1422
- entry.data.date ?? entry.data.changelog?.date ?? null;
1507
+ const entryDate = (entry: {
1508
+ data: { date?: string | null; changelog?: { date?: string | null } | null };
1509
+ }) => entry.data.date ?? entry.data.changelog?.date ?? null;
1423
1510
 
1424
- const toTime = (value) => {
1511
+ const toTime = (value: string | null | undefined) => {
1425
1512
  if (!value) {
1426
1513
  return 0;
1427
1514
  }
@@ -1429,27 +1516,39 @@ const toTime = (value) => {
1429
1516
  return Number.isNaN(date.getTime()) ? 0 : date.getTime();
1430
1517
  };
1431
1518
 
1432
- const formatDate = (value) => {
1519
+ // The changelog is an unlocalized route, so its chrome renders in the default
1520
+ // locale's dictionary and direction (\`data.ui\` is the default locale's resolved
1521
+ // dictionary), mirroring the catch-all's locale wiring.
1522
+ const i18n = data.config.i18n;
1523
+ const localeMeta = i18n
1524
+ ? i18n.locales.find((l) => l.code === i18n.defaultLocale)
1525
+ : null;
1526
+ const dir = localeMeta?.dir ?? "ltr";
1527
+ const htmlLang = i18n ? i18n.defaultLocale : "en";
1528
+
1529
+ // Formatted in the same locale as the chrome, and in UTC, to match the
1530
+ // per-page "last updated" stamp.
1531
+ const formatDate = (value: string | null | undefined) => {
1433
1532
  if (!value) {
1434
1533
  return;
1435
1534
  }
1436
1535
  const date = new Date(value);
1437
1536
  return Number.isNaN(date.getTime())
1438
1537
  ? undefined
1439
- : new Intl.DateTimeFormat("en", {
1538
+ : new Intl.DateTimeFormat(htmlLang, {
1440
1539
  dateStyle: "long",
1441
1540
  timeZone: "UTC",
1442
1541
  }).format(date);
1443
1542
  };
1444
1543
 
1445
- const slugify = (text) =>
1544
+ const slugify = (text: string) =>
1446
1545
  text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ||
1447
1546
  "update";
1448
1547
 
1449
1548
  // The major of a version's embedded semver (\`1.2.3\` -> 1, \`pkg@2.0.0\` -> 2), or
1450
1549
  // null when there is no full major.minor.patch to key on. Drives the changelog's
1451
1550
  // group-by-major pagination, so it tolerates the scoped tags monorepos publish.
1452
- const majorVersion = (version) => {
1551
+ const majorVersion = (version: string | null | undefined) => {
1453
1552
  const match = /(\\d+)\\.\\d+\\.\\d+/.exec(String(version ?? ""));
1454
1553
  return match ? Number(match[1]) : null;
1455
1554
  };
@@ -1481,7 +1580,7 @@ const items = await Promise.all(
1481
1580
  return {
1482
1581
  Content: (await render(entry)).Content,
1483
1582
  date: formatDate(entryDate(entry)),
1484
- href: routeByEntry.get(entry.id) ?? null,
1583
+ href: routeByEntry.get(entry.id) ?? undefined,
1485
1584
  id: slugify(label),
1486
1585
  label,
1487
1586
  major: majorVersion(entry.data.changelog?.version),
@@ -1510,7 +1609,9 @@ for (const item of items) {
1510
1609
  // semver and they span more than one major line. Older majors then collapse
1511
1610
  // into groups the reader reveals one at a time; otherwise the timeline is flat.
1512
1611
  const majors = items.every((item) => item.major !== null)
1513
- ? [...new Set(items.map((item) => item.major))].toSorted((a, b) => b - a)
1612
+ ? [...new Set(items.map((item) => item.major))]
1613
+ .filter((major): major is number => major !== null)
1614
+ .toSorted((a, b) => b - a)
1514
1615
  : [];
1515
1616
  const paginate = majors.length > 1;
1516
1617
  const majorGroups = majors.map((major) => ({
@@ -1531,16 +1632,6 @@ const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
1531
1632
  const basedRoute = withBase("/changelog");
1532
1633
  const canonical = base ? base + basedRoute : null;
1533
1634
 
1534
- // The changelog is an unlocalized route, so its chrome renders in the default
1535
- // locale's dictionary and direction (\`data.ui\` is the default locale's resolved
1536
- // dictionary), mirroring the catch-all's locale wiring.
1537
- const i18n = data.config.i18n;
1538
- const localeMeta = i18n
1539
- ? i18n.locales.find((l) => l.code === i18n.defaultLocale)
1540
- : null;
1541
- const dir = localeMeta?.dir ?? "ltr";
1542
- const htmlLang = i18n ? i18n.defaultLocale : "en";
1543
-
1544
1635
  // The page chrome (h1, title, description) comes from the same translatable
1545
1636
  // \`changelog\` group as the reveal button; optional chaining tolerates a
1546
1637
  // not-yet-regenerated data snapshot from before these keys existed.
@@ -1658,7 +1749,7 @@ export const notFoundPageTemplate = (): string => `---
1658
1749
  // Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
1659
1750
  import PageLayout from "blume/components/layout/PageLayout.astro";
1660
1751
  import { withBase } from "blume/components/islands/base-path.ts";
1661
- import data from "../generated/data.json";
1752
+ import data from "blume:data";
1662
1753
 
1663
1754
  export const prerender = true;
1664
1755
 
@@ -1867,7 +1958,10 @@ export const getStaticPaths = () =>
1867
1958
  Object.keys(examples).map((path) => ({ params: { path } }));
1868
1959
 
1869
1960
  const { path } = Astro.params;
1870
- const entry = examples[path];
1961
+ const entry = path ? examples[path] : undefined;
1962
+ if (!entry) {
1963
+ return new Response(null, { status: 404 });
1964
+ }
1871
1965
  const Example = entry.Component;
1872
1966
  ---
1873
1967
 
@@ -1924,6 +2018,14 @@ declare module "blume:data" {
1924
2018
  export default data;
1925
2019
  }
1926
2020
 
2021
+ declare module "blume:examples" {
2022
+ type Examples = typeof import("./generated/examples.ts").examples;
2023
+ export const examples: Record<string, Examples[keyof Examples]>;
2024
+ export const examplesBase: string;
2025
+ }
2026
+
2027
+ declare module "blume:examples-theme";
2028
+
1927
2029
  declare module "blume:openapi" {
1928
2030
  const specs: import("blume/openapi/model.ts").OpenApiData;
1929
2031
  export default specs;
@@ -0,0 +1,114 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtemp, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+
5
+ import { join } from "pathe";
6
+
7
+ import { reportJson } from "./report.ts";
8
+ import type { AuditResult } from "./run.ts";
9
+
10
+ /** A coding agent CLI the audit can hand its findings to (`--claude`, `--codex`). */
11
+ export interface AgentCli {
12
+ /** The executable to look up on PATH. */
13
+ bin: string;
14
+ /** How to install it, shown when the executable is missing. */
15
+ install: string;
16
+ /** Display name for messages. */
17
+ name: string;
18
+ }
19
+
20
+ export type AgentKind = "claude" | "codex";
21
+
22
+ export const AGENTS: Record<AgentKind, AgentCli> = {
23
+ claude: {
24
+ bin: "claude",
25
+ install: "npm install -g @anthropic-ai/claude-code",
26
+ name: "Claude Code",
27
+ },
28
+ codex: {
29
+ bin: "codex",
30
+ install: "npm install -g @openai/codex",
31
+ name: "Codex",
32
+ },
33
+ };
34
+
35
+ /**
36
+ * Write the full JSON report where the agent can read it. A file rather than
37
+ * inline prompt text: a large site's report can exceed the platform's argv
38
+ * limit, and the JSON already carries every finding untruncated — the terminal
39
+ * report previews three pages per check, the file never does.
40
+ */
41
+ export const writeAgentReport = async (
42
+ result: AuditResult,
43
+ root: string
44
+ ): Promise<string> => {
45
+ const dir = await mkdtemp(join(tmpdir(), "blume-audit-"));
46
+ const path = join(dir, "report.json");
47
+ await writeFile(path, reportJson(result, root));
48
+ return path;
49
+ };
50
+
51
+ /** The handoff prompt: where the report is, how to read it, and the ground rules. */
52
+ export const fixPrompt = (reportPath: string): string =>
53
+ `Fix the issues found by \`blume audit\` in this project.
54
+
55
+ The full audit report is at ${reportPath}. It is JSON: each entry in \`diagnostics\` is one finding, with the check \`code\`, a \`message\` explaining what is wrong, the affected page \`url\`, the source \`file\` to edit (relative to the current directory, with a \`line\` when the finding points at a specific front matter key), and a \`suggestion\` describing the fix.
56
+
57
+ Work through every finding:
58
+ 1. Read the report and group the findings by \`file\`.
59
+ 2. Apply each finding's \`suggestion\` by editing the named source file — most fixes are front matter edits at the cited line.
60
+ 3. Never fix a finding by deleting a page, removing content, or hiding it from the audit; if a finding genuinely needs a human decision, leave it and say so in your summary.
61
+
62
+ When you are done, run \`blume build\` and then \`blume audit\` to verify, and repeat until the audit reports no issues.`;
63
+
64
+ const spawnAgent = (
65
+ command: string,
66
+ args: string[],
67
+ shell: boolean
68
+ ): Promise<number> =>
69
+ // oxlint-disable-next-line promise/avoid-new -- adapt spawn's event callbacks
70
+ new Promise((resolve, reject) => {
71
+ const child = spawn(command, args, { shell, stdio: "inherit" });
72
+ child.once("error", reject);
73
+ child.once("close", (code) => resolve(code ?? 1));
74
+ });
75
+
76
+ /**
77
+ * cmd.exe reports a missing executable through this exit code instead of a
78
+ * spawn error, so a shell launch can't rely on the `error` event for the
79
+ * "not installed" diagnosis.
80
+ */
81
+ export const WINDOWS_COMMAND_NOT_FOUND = 9009;
82
+
83
+ /**
84
+ * Run the agent CLI interactively with the handoff prompt, inheriting the
85
+ * terminal so the user watches and steers the fixes rather than granting a
86
+ * headless process blanket write access. Resolves with the agent's exit code;
87
+ * rejects when the executable isn't on PATH.
88
+ *
89
+ * On Windows, npm installs agent CLIs as `.cmd` shims, which Node refuses to
90
+ * spawn without a shell — and cmd.exe cannot carry the multi-line prompt as an
91
+ * argument (a newline ends the command). So there the prompt is written to a
92
+ * file next to the report and handed over via a one-line pointer that survives
93
+ * cmd.exe quoting; a missing executable surfaces as
94
+ * {@link WINDOWS_COMMAND_NOT_FOUND} rather than a rejection.
95
+ */
96
+ export const launchAgent = async (
97
+ bin: string,
98
+ prompt: string,
99
+ platform: NodeJS.Platform = process.platform
100
+ ): Promise<number> => {
101
+ if (platform !== "win32") {
102
+ return await spawnAgent(bin, [prompt], false);
103
+ }
104
+ const dir = await mkdtemp(join(tmpdir(), "blume-audit-"));
105
+ const promptPath = join(dir, "prompt.md");
106
+ await writeFile(promptPath, prompt);
107
+ // Double quotes are the one grouping cmd.exe respects; neither the temp
108
+ // path nor the fixed pointer text can contain one.
109
+ return await spawnAgent(
110
+ `"${bin}" "Read ${promptPath} and follow its instructions exactly."`,
111
+ [],
112
+ true
113
+ );
114
+ };