blume 0.0.0 → 0.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 (263) hide show
  1. package/bin/blume.mjs +18 -0
  2. package/dist/cli/index.js +11989 -0
  3. package/dist/cli/index.js.map +141 -0
  4. package/docs/01-quickstart.mdx +99 -0
  5. package/docs/02-deployment.mdx +129 -0
  6. package/docs/advanced/api-reference.mdx +114 -0
  7. package/docs/advanced/blog.mdx +121 -0
  8. package/docs/advanced/changelog.mdx +113 -0
  9. package/docs/advanced/custom-pages.mdx +187 -0
  10. package/docs/advanced/meta.ts +7 -0
  11. package/docs/changelog/v0-1-0.mdx +12 -0
  12. package/docs/changelog/v0-2-0.mdx +16 -0
  13. package/docs/configuration/ai.mdx +228 -0
  14. package/docs/configuration/analytics.mdx +98 -0
  15. package/docs/configuration/customization.mdx +91 -0
  16. package/docs/configuration/export.mdx +70 -0
  17. package/docs/configuration/index.mdx +271 -0
  18. package/docs/configuration/meta.ts +15 -0
  19. package/docs/configuration/search.mdx +172 -0
  20. package/docs/configuration/seo.mdx +196 -0
  21. package/docs/configuration/theming.mdx +178 -0
  22. package/docs/content/components.mdx +565 -0
  23. package/docs/content/i18n.mdx +205 -0
  24. package/docs/content/index.mdx +161 -0
  25. package/docs/content/islands.mdx +94 -0
  26. package/docs/content/meta.mdx +119 -0
  27. package/docs/content/meta.ts +15 -0
  28. package/docs/content/navigation.mdx +168 -0
  29. package/docs/content/sources.mdx +216 -0
  30. package/docs/content/syntax.mdx +445 -0
  31. package/docs/index.mdx +112 -0
  32. package/docs/reference/cli.mdx +43 -0
  33. package/docs/reference/frontmatter.mdx +74 -0
  34. package/docs/reference/meta.ts +7 -0
  35. package/package.json +140 -6
  36. package/src/ai/ask.ts +93 -0
  37. package/src/ai/llms.ts +65 -0
  38. package/src/ai/markdown.ts +31 -0
  39. package/src/ai/mcp/data.ts +74 -0
  40. package/src/ai/mcp/discovery.ts +49 -0
  41. package/src/ai/mcp/server.ts +225 -0
  42. package/src/ai/mcp/tools.ts +47 -0
  43. package/src/assets/icon.png +0 -0
  44. package/src/astro/generate.ts +878 -0
  45. package/src/astro/index.ts +4 -0
  46. package/src/astro/integration.ts +74 -0
  47. package/src/astro/islands.ts +131 -0
  48. package/src/astro/markdown-negotiation.ts +68 -0
  49. package/src/astro/pages.ts +28 -0
  50. package/src/astro/templates.ts +1199 -0
  51. package/src/cli/commands/add.ts +81 -0
  52. package/src/cli/commands/build.ts +103 -0
  53. package/src/cli/commands/dev.ts +108 -0
  54. package/src/cli/commands/doctor.ts +74 -0
  55. package/src/cli/commands/eject.ts +57 -0
  56. package/src/cli/commands/init.ts +98 -0
  57. package/src/cli/commands/migrate.ts +39 -0
  58. package/src/cli/commands/preview.ts +39 -0
  59. package/src/cli/commands/sync.ts +52 -0
  60. package/src/cli/commands/validate.ts +60 -0
  61. package/src/cli/index.ts +35 -0
  62. package/src/cli/log.ts +37 -0
  63. package/src/cli/prepare.ts +80 -0
  64. package/src/components/Icon.astro +99 -0
  65. package/src/components/content/Accordion.astro +8 -0
  66. package/src/components/content/AccordionItem.astro +121 -0
  67. package/src/components/content/AutoTypeTable.astro +51 -0
  68. package/src/components/content/Badge.astro +124 -0
  69. package/src/components/content/Callout.astro +73 -0
  70. package/src/components/content/Card.astro +104 -0
  71. package/src/components/content/CardGroup.astro +14 -0
  72. package/src/components/content/CodeGroup.astro +13 -0
  73. package/src/components/content/Color.astro +15 -0
  74. package/src/components/content/ColorItem.astro +87 -0
  75. package/src/components/content/ColorRow.astro +10 -0
  76. package/src/components/content/Column.astro +6 -0
  77. package/src/components/content/Columns.astro +9 -0
  78. package/src/components/content/Expandable.astro +11 -0
  79. package/src/components/content/FileTree.astro +8 -0
  80. package/src/components/content/Frame.astro +70 -0
  81. package/src/components/content/GithubInfo.astro +110 -0
  82. package/src/components/content/Math.astro +24 -0
  83. package/src/components/content/Panel.astro +20 -0
  84. package/src/components/content/Prompt.astro +129 -0
  85. package/src/components/content/Step.astro +34 -0
  86. package/src/components/content/Steps.astro +20 -0
  87. package/src/components/content/Tab.astro +40 -0
  88. package/src/components/content/Tabs.astro +273 -0
  89. package/src/components/content/Tile.astro +42 -0
  90. package/src/components/content/Tooltip.astro +68 -0
  91. package/src/components/content/Tree.astro +300 -0
  92. package/src/components/content/TreeFile.astro +15 -0
  93. package/src/components/content/TreeFolder.astro +62 -0
  94. package/src/components/content/TypeTable.astro +106 -0
  95. package/src/components/content/Update.astro +66 -0
  96. package/src/components/content/Visibility.astro +12 -0
  97. package/src/components/content/Warning.astro +9 -0
  98. package/src/components/content/auto-type-table.ts +141 -0
  99. package/src/components/content/github-info.ts +79 -0
  100. package/src/components/content/mermaid-element.ts +68 -0
  101. package/src/components/github-mark.ts +9 -0
  102. package/src/components/index.ts +14 -0
  103. package/src/components/islands/AskAI.astro +12 -0
  104. package/src/components/islands/ask-ai.tsx +156 -0
  105. package/src/components/layout/Analytics.astro +63 -0
  106. package/src/components/layout/Banner.astro +50 -0
  107. package/src/components/layout/Breadcrumbs.astro +31 -0
  108. package/src/components/layout/Favicon.astro +15 -0
  109. package/src/components/layout/Fonts.astro +14 -0
  110. package/src/components/layout/Header.astro +188 -0
  111. package/src/components/layout/LanguageSwitcher.astro +56 -0
  112. package/src/components/layout/NavTree.astro +462 -0
  113. package/src/components/layout/PageActions.astro +438 -0
  114. package/src/components/layout/PageFeedback.astro +58 -0
  115. package/src/components/layout/Pagination.astro +56 -0
  116. package/src/components/layout/ReferenceLayout.astro +102 -0
  117. package/src/components/layout/RootLayout.astro +533 -0
  118. package/src/components/layout/Search.astro +608 -0
  119. package/src/components/layout/TableOfContents.astro +68 -0
  120. package/src/components/layout/analytics-client.ts +38 -0
  121. package/src/components/layout/nav-utils.ts +87 -0
  122. package/src/components/layout/overrides.ts +32 -0
  123. package/src/components/layout/search/algolia.ts +43 -0
  124. package/src/components/layout/search/endpoint.ts +22 -0
  125. package/src/components/layout/search/flexsearch.ts +52 -0
  126. package/src/components/layout/search/orama-cloud.ts +41 -0
  127. package/src/components/layout/search/orama.ts +26 -0
  128. package/src/components/layout/search/pagefind.ts +43 -0
  129. package/src/components/layout/search/types.ts +163 -0
  130. package/src/components/layout/search/typesense.ts +60 -0
  131. package/src/components/layout/toc-element.ts +108 -0
  132. package/src/core/bridge.ts +92 -0
  133. package/src/core/config.ts +112 -0
  134. package/src/core/content.ts +50 -0
  135. package/src/core/define-components.ts +34 -0
  136. package/src/core/define-meta.ts +20 -0
  137. package/src/core/deployment-env.ts +73 -0
  138. package/src/core/diagnostics.ts +104 -0
  139. package/src/core/graph.ts +128 -0
  140. package/src/core/i18n-ui.ts +171 -0
  141. package/src/core/i18n.ts +169 -0
  142. package/src/core/last-modified.ts +88 -0
  143. package/src/core/links.ts +336 -0
  144. package/src/core/load-module.ts +15 -0
  145. package/src/core/manifest.ts +126 -0
  146. package/src/core/meta.ts +97 -0
  147. package/src/core/navigation.ts +392 -0
  148. package/src/core/package-root.ts +37 -0
  149. package/src/core/project-graph.ts +153 -0
  150. package/src/core/project.ts +56 -0
  151. package/src/core/schema.ts +1057 -0
  152. package/src/core/server-features.ts +23 -0
  153. package/src/core/sources/assets.ts +77 -0
  154. package/src/core/sources/cache.ts +122 -0
  155. package/src/core/sources/filesystem.ts +99 -0
  156. package/src/core/sources/mdx-remote.ts +216 -0
  157. package/src/core/sources/mintlify.ts +161 -0
  158. package/src/core/sources/normalize.ts +227 -0
  159. package/src/core/sources/notion.ts +440 -0
  160. package/src/core/sources/portable-text.ts +143 -0
  161. package/src/core/sources/read.ts +36 -0
  162. package/src/core/sources/resolve.ts +158 -0
  163. package/src/core/sources/sanity.ts +218 -0
  164. package/src/core/sources/types.ts +105 -0
  165. package/src/core/types.ts +261 -0
  166. package/src/core/ui-packs/ar.ts +47 -0
  167. package/src/core/ui-packs/bg.ts +47 -0
  168. package/src/core/ui-packs/bn.ts +47 -0
  169. package/src/core/ui-packs/ca.ts +47 -0
  170. package/src/core/ui-packs/cs.ts +47 -0
  171. package/src/core/ui-packs/da.ts +47 -0
  172. package/src/core/ui-packs/de.ts +47 -0
  173. package/src/core/ui-packs/el.ts +47 -0
  174. package/src/core/ui-packs/es.ts +47 -0
  175. package/src/core/ui-packs/fa.ts +47 -0
  176. package/src/core/ui-packs/fi.ts +47 -0
  177. package/src/core/ui-packs/fr.ts +47 -0
  178. package/src/core/ui-packs/he.ts +47 -0
  179. package/src/core/ui-packs/hi.ts +47 -0
  180. package/src/core/ui-packs/hr.ts +47 -0
  181. package/src/core/ui-packs/hu.ts +47 -0
  182. package/src/core/ui-packs/id.ts +47 -0
  183. package/src/core/ui-packs/index.ts +87 -0
  184. package/src/core/ui-packs/it.ts +47 -0
  185. package/src/core/ui-packs/ja.ts +47 -0
  186. package/src/core/ui-packs/ko.ts +47 -0
  187. package/src/core/ui-packs/nl.ts +47 -0
  188. package/src/core/ui-packs/no.ts +47 -0
  189. package/src/core/ui-packs/pl.ts +47 -0
  190. package/src/core/ui-packs/pt-br.ts +47 -0
  191. package/src/core/ui-packs/pt.ts +47 -0
  192. package/src/core/ui-packs/ro.ts +47 -0
  193. package/src/core/ui-packs/ru.ts +47 -0
  194. package/src/core/ui-packs/sk.ts +47 -0
  195. package/src/core/ui-packs/sr.ts +47 -0
  196. package/src/core/ui-packs/sv.ts +47 -0
  197. package/src/core/ui-packs/th.ts +47 -0
  198. package/src/core/ui-packs/tr.ts +47 -0
  199. package/src/core/ui-packs/uk.ts +47 -0
  200. package/src/core/ui-packs/vi.ts +47 -0
  201. package/src/core/ui-packs/zh-tw.ts +47 -0
  202. package/src/core/ui-packs/zh.ts +47 -0
  203. package/src/core/version.ts +23 -0
  204. package/src/deploy/robots.ts +20 -0
  205. package/src/deploy/rss.ts +128 -0
  206. package/src/deploy/sitemap.ts +28 -0
  207. package/src/index.ts +27 -0
  208. package/src/markdown/code-title.ts +71 -0
  209. package/src/markdown/directives.ts +83 -0
  210. package/src/markdown/heading-anchors.ts +137 -0
  211. package/src/markdown/index.ts +159 -0
  212. package/src/markdown/inline-code.ts +108 -0
  213. package/src/markdown/language-icon.ts +172 -0
  214. package/src/markdown/math.ts +32 -0
  215. package/src/markdown/mdast.ts +48 -0
  216. package/src/markdown/mermaid.ts +37 -0
  217. package/src/markdown/package-commands.ts +159 -0
  218. package/src/markdown/package-install.ts +40 -0
  219. package/src/migrate/fumadocs/config.ts +106 -0
  220. package/src/migrate/fumadocs/content.ts +365 -0
  221. package/src/migrate/fumadocs/frontmatter.ts +18 -0
  222. package/src/migrate/fumadocs/index.ts +252 -0
  223. package/src/migrate/fumadocs/meta.ts +114 -0
  224. package/src/migrate/migrate.ts +53 -0
  225. package/src/migrate/mintlify/config.ts +1040 -0
  226. package/src/migrate/mintlify/content.ts +98 -0
  227. package/src/migrate/mintlify/frontmatter.ts +126 -0
  228. package/src/migrate/mintlify/i18n.ts +51 -0
  229. package/src/migrate/mintlify/icons.ts +128 -0
  230. package/src/migrate/mintlify/index.ts +266 -0
  231. package/src/migrate/mintlify/snippets.ts +305 -0
  232. package/src/migrate/mintlify/transform.ts +81 -0
  233. package/src/migrate/nextra/content.ts +46 -0
  234. package/src/migrate/nextra/frontmatter.ts +40 -0
  235. package/src/migrate/nextra/index.ts +374 -0
  236. package/src/migrate/nextra/meta.ts +266 -0
  237. package/src/migrate/shared.ts +623 -0
  238. package/src/migrate/starlight/config.ts +459 -0
  239. package/src/migrate/starlight/content.ts +78 -0
  240. package/src/migrate/starlight/frontmatter.ts +111 -0
  241. package/src/migrate/starlight/i18n.ts +54 -0
  242. package/src/migrate/starlight/index.ts +131 -0
  243. package/src/og/card.ts +92 -0
  244. package/src/og/index.ts +2 -0
  245. package/src/openapi/scalar.ts +246 -0
  246. package/src/registry/eject.ts +263 -0
  247. package/src/registry/registry.ts +100 -0
  248. package/src/registry/rewrite-imports.ts +39 -0
  249. package/src/runtime/index.ts +14 -0
  250. package/src/search/build.ts +23 -0
  251. package/src/search/documents.ts +165 -0
  252. package/src/search/orama-index.ts +66 -0
  253. package/src/search/providers.ts +91 -0
  254. package/src/search/sync/algolia.ts +30 -0
  255. package/src/search/sync/index.ts +50 -0
  256. package/src/search/sync/orama-cloud.ts +40 -0
  257. package/src/search/sync/typesense.ts +65 -0
  258. package/src/seo/jsonld.ts +113 -0
  259. package/src/theme/entry.ts +608 -0
  260. package/src/theme/fonts.ts +198 -0
  261. package/src/theme/icons.ts +184 -0
  262. package/src/theme/palette.ts +143 -0
  263. package/src/theme/twoslash.ts +81 -0
@@ -0,0 +1,81 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+
4
+ import { defineCommand } from "citty";
5
+ import { dirname, join } from "pathe";
6
+
7
+ import { findItem, packageSrc, registry } from "../../registry/registry.ts";
8
+ import { rewriteImports } from "../../registry/rewrite-imports.ts";
9
+ import { logger } from "../log.ts";
10
+
11
+ export const addCommand = defineCommand({
12
+ args: {
13
+ force: { description: "Overwrite existing files.", type: "boolean" },
14
+ name: {
15
+ description: "Registry item to install.",
16
+ required: false,
17
+ type: "positional",
18
+ },
19
+ },
20
+ meta: {
21
+ description: "Install a source component or template from the registry.",
22
+ name: "add",
23
+ },
24
+ async run({ args }) {
25
+ const root = process.cwd();
26
+
27
+ if (!args.name) {
28
+ logger.info("Available registry items:");
29
+ for (const item of registry) {
30
+ process.stdout.write(` ${item.name} — ${item.description}\n`);
31
+ }
32
+ return;
33
+ }
34
+
35
+ const item = findItem(args.name);
36
+ if (!item) {
37
+ logger.error(`Unknown registry item: ${args.name}`);
38
+ logger.info(`Run \`blume add\` to list available items.`);
39
+ process.exit(1);
40
+ }
41
+
42
+ const plan = item.files.map((file) => ({
43
+ file,
44
+ skip: existsSync(join(root, file.target)) && !args.force,
45
+ target: join(root, file.target),
46
+ }));
47
+
48
+ await Promise.all(
49
+ plan
50
+ .filter((entry) => !entry.skip)
51
+ .map(async (entry) => {
52
+ const source = join(packageSrc, entry.file.source);
53
+ const raw = await readFile(source, "utf-8");
54
+ // Built-in components carry relative imports into the package; rewrite
55
+ // them to `blume/*` so the installed copy resolves them.
56
+ const content = entry.file.rewrite
57
+ ? rewriteImports(raw, source, packageSrc)
58
+ : raw;
59
+ await mkdir(dirname(entry.target), { recursive: true });
60
+ await writeFile(entry.target, content, "utf-8");
61
+ })
62
+ );
63
+
64
+ for (const entry of plan) {
65
+ if (entry.skip) {
66
+ logger.warn(
67
+ `Skipped existing ${entry.file.target} (use --force to overwrite)`
68
+ );
69
+ } else {
70
+ logger.success(`Added ${entry.file.target}`);
71
+ }
72
+ }
73
+
74
+ if (item.postInstall.length > 0) {
75
+ process.stdout.write(`\nNext steps:\n`);
76
+ for (const line of item.postInstall) {
77
+ process.stdout.write(` ${line}\n`);
78
+ }
79
+ }
80
+ },
81
+ });
@@ -0,0 +1,103 @@
1
+ import { existsSync } from "node:fs";
2
+ import { writeFile } from "node:fs/promises";
3
+
4
+ import { build } from "astro";
5
+ import { defineCommand } from "citty";
6
+ import { join } from "pathe";
7
+
8
+ import { buildLlmsFiles } from "../../ai/llms.ts";
9
+ import { serverFeatures } from "../../core/server-features.ts";
10
+ import { buildRobots } from "../../deploy/robots.ts";
11
+ import { buildSitemap } from "../../deploy/sitemap.ts";
12
+ import { buildSearchIndex } from "../../search/build.ts";
13
+ import { syncSearchProvider } from "../../search/sync/index.ts";
14
+ import { logger } from "../log.ts";
15
+ import { prepareProject } from "../prepare.ts";
16
+
17
+ export const buildCommand = defineCommand({
18
+ args: {
19
+ preview: {
20
+ description: "Include drafts and unpublished CMS content.",
21
+ type: "boolean",
22
+ },
23
+ strict: { description: "Fail on diagnostics.", type: "boolean" },
24
+ },
25
+ meta: {
26
+ description: "Build the docs site for production.",
27
+ name: "build",
28
+ },
29
+ async run({ args }) {
30
+ const root = process.cwd();
31
+ const project = await prepareProject({
32
+ mode: "build",
33
+ preview: args.preview,
34
+ root,
35
+ strict: args.strict,
36
+ });
37
+
38
+ logger.start(
39
+ `Building ${project.graph.pages.length} page(s) (${project.config.deployment.output} output)`
40
+ );
41
+
42
+ await build({
43
+ logLevel: "info",
44
+ root: project.context.outDir,
45
+ });
46
+
47
+ const distDir = join(root, "dist");
48
+
49
+ if (project.config.search.provider === "pagefind") {
50
+ logger.start("Building search index");
51
+ const indexed = await buildSearchIndex(distDir);
52
+ logger.success(`Indexed ${indexed} page(s) for search`);
53
+ }
54
+
55
+ // Upload the index to a hosted provider (Algolia, Orama Cloud, Typesense).
56
+ // Skipped with a warning when its admin key isn't configured.
57
+ await syncSearchProvider(project, {
58
+ start: (message) => logger.start(message),
59
+ success: (message) => logger.success(message),
60
+ warn: (message) => logger.warn(message),
61
+ });
62
+
63
+ if (project.config.ai.llmsTxt) {
64
+ const { index, full } = await buildLlmsFiles(project);
65
+ await Promise.all([
66
+ writeFile(join(distDir, "llms.txt"), index, "utf-8"),
67
+ writeFile(join(distDir, "llms-full.txt"), full, "utf-8"),
68
+ ]);
69
+ logger.success("Generated llms.txt and llms-full.txt");
70
+ }
71
+
72
+ // A user's own public/ file (copied into dist by Astro) always wins.
73
+ const sitemap = buildSitemap(project);
74
+ if (sitemap && !existsSync(join(distDir, "sitemap.xml"))) {
75
+ await writeFile(join(distDir, "sitemap.xml"), sitemap, "utf-8");
76
+ logger.success("Generated sitemap.xml");
77
+ }
78
+
79
+ const robots = buildRobots(project);
80
+ if (robots && !existsSync(join(distDir, "robots.txt"))) {
81
+ await writeFile(join(distDir, "robots.txt"), robots, "utf-8");
82
+ logger.success("Generated robots.txt");
83
+ }
84
+
85
+ const { config } = project;
86
+ const features = serverFeatures(config);
87
+ logger.box(
88
+ [
89
+ `Output ${config.deployment.output}`,
90
+ `Adapter ${config.deployment.adapter ?? "none"}`,
91
+ `Site ${config.deployment.site ?? "not set"}`,
92
+ `Search ${config.search.provider}`,
93
+ `Redirects ${config.redirects.length}`,
94
+ `Sitemap ${sitemap ? "yes" : "no (set deployment.site)"}`,
95
+ `Robots ${robots ? "yes" : "no"}`,
96
+ `LLM files ${config.ai.llmsTxt ? "yes" : "no"}`,
97
+ `Server features ${features.length > 0 ? features.join(", ") : "none"}`,
98
+ ].join("\n")
99
+ );
100
+
101
+ logger.success(`Built to ${distDir}`);
102
+ },
103
+ });
@@ -0,0 +1,108 @@
1
+ import { watch } from "node:fs";
2
+
3
+ import { dev } from "astro";
4
+ import { defineCommand } from "citty";
5
+
6
+ import { generateRuntime } from "../../astro/generate.ts";
7
+ import { scanProject } from "../../core/project-graph.ts";
8
+ import { logger } from "../log.ts";
9
+ import { prepareProject } from "../prepare.ts";
10
+
11
+ export const devCommand = defineCommand({
12
+ args: {
13
+ host: { description: "Network host to bind.", type: "string" },
14
+ open: { description: "Open the browser on start.", type: "boolean" },
15
+ port: { description: "Port to listen on.", type: "string" },
16
+ preview: {
17
+ description: "Include drafts and unpublished CMS content.",
18
+ type: "boolean",
19
+ },
20
+ strict: { description: "Fail on diagnostics.", type: "boolean" },
21
+ },
22
+ meta: {
23
+ description: "Start the Blume development server.",
24
+ name: "dev",
25
+ },
26
+ async run({ args }) {
27
+ const root = process.cwd();
28
+ const preview = args.preview ?? false;
29
+ // Astro's dev server defaults to 4321 when no port is passed. Feeding the
30
+ // resolved URL in as the `deployment.site` fallback lets site-gated features
31
+ // (OG images, canonicals, sitemap) work locally without configuring a site.
32
+ const port = args.port ? Number(args.port) : 4321;
33
+ const devServerUrl = `http://localhost:${port}`;
34
+ const project = await prepareProject({
35
+ devServerUrl,
36
+ mode: "dev",
37
+ preview,
38
+ root,
39
+ strict: args.strict,
40
+ });
41
+
42
+ if (project.bridge) {
43
+ logger.info(
44
+ 'Detected docs.json — running in Mintlify bridge mode (no migration). Run "blume migrate mintlify" to convert permanently.'
45
+ );
46
+ }
47
+
48
+ const server = await dev({
49
+ logLevel: "info",
50
+ root: project.context.outDir,
51
+ server: {
52
+ host: args.host ?? false,
53
+ open: args.open ?? false,
54
+ port: args.port ? Number(args.port) : undefined,
55
+ },
56
+ });
57
+
58
+ // Watch user inputs and regenerate the runtime data on change. Astro/Vite
59
+ // hot-reloads the generated data module so nav and routes stay in sync.
60
+ let timer: ReturnType<typeof setTimeout> | null = null;
61
+ const regenerate = () => {
62
+ if (timer) {
63
+ clearTimeout(timer);
64
+ }
65
+ timer = setTimeout(async () => {
66
+ try {
67
+ const next = await scanProject(root, {
68
+ devServerUrl,
69
+ mode: "dev",
70
+ preview,
71
+ });
72
+ await generateRuntime(next);
73
+ } catch (error) {
74
+ logger.error(`Regeneration failed: ${(error as Error).message}`);
75
+ }
76
+ }, 80);
77
+ };
78
+
79
+ // Content is watched per source (filesystem uses fs.watch; remote sources
80
+ // are frozen for the session). The remaining project inputs — user pages,
81
+ // config, theme, and component overrides — are watched directly.
82
+ const fileTargets = [
83
+ project.context.pagesRoot,
84
+ project.context.configFile,
85
+ project.context.themeFile,
86
+ project.context.componentsFile,
87
+ ].filter((target) => target !== null);
88
+
89
+ const disposers = [
90
+ ...project.sources.map((source) => source.watch?.(regenerate)),
91
+ ...fileTargets.map((target) => {
92
+ const watcher = watch(target, { recursive: true }, regenerate);
93
+ return () => watcher.close();
94
+ }),
95
+ ].filter((dispose) => dispose !== undefined);
96
+
97
+ const shutdown = async () => {
98
+ for (const dispose of disposers) {
99
+ dispose();
100
+ }
101
+ await server.stop();
102
+ process.exit(0);
103
+ };
104
+
105
+ process.on("SIGINT", shutdown);
106
+ process.on("SIGTERM", shutdown);
107
+ },
108
+ });
@@ -0,0 +1,74 @@
1
+ import { defineCommand } from "citty";
2
+
3
+ import { BlumeError } from "../../core/diagnostics.ts";
4
+ import { scanProject } from "../../core/project-graph.ts";
5
+ import { serverFeatures } from "../../core/server-features.ts";
6
+ import type { Diagnostic } from "../../core/types.ts";
7
+ import { logger, reportDiagnostics } from "../log.ts";
8
+
9
+ const MIN_NODE_MAJOR = 20;
10
+
11
+ export const doctorCommand = defineCommand({
12
+ meta: {
13
+ description: "Diagnose common configuration and content problems.",
14
+ name: "doctor",
15
+ },
16
+ async run() {
17
+ const root = process.cwd();
18
+ const diagnostics: Diagnostic[] = [];
19
+
20
+ const nodeMajor = Number.parseInt(
21
+ process.versions.node.split(".")[0] ?? "0",
22
+ 10
23
+ );
24
+ if (nodeMajor < MIN_NODE_MAJOR) {
25
+ diagnostics.push({
26
+ code: "BLUME_NODE_VERSION",
27
+ message: `Node ${process.versions.node} is below the supported minimum (${MIN_NODE_MAJOR}).`,
28
+ severity: "warning",
29
+ });
30
+ }
31
+
32
+ try {
33
+ const project = await scanProject(root, { mode: "build" });
34
+ diagnostics.push(...project.diagnostics);
35
+
36
+ const { config } = project;
37
+ const features = serverFeatures(config);
38
+ if (features.length > 0 && config.deployment.output === "static") {
39
+ diagnostics.push({
40
+ code: "BLUME_SERVER_FEATURE_REQUIRED",
41
+ message: `${features.join(", ")} require server output.`,
42
+ severity: "error",
43
+ suggestion: 'Set deployment.output to "server".',
44
+ });
45
+ }
46
+ if (config.deployment.output === "server" && !config.deployment.adapter) {
47
+ diagnostics.push({
48
+ code: "BLUME_ADAPTER_REQUIRED",
49
+ message: "Server output requires an adapter.",
50
+ severity: "error",
51
+ suggestion: 'Set deployment.adapter (e.g. "vercel").',
52
+ });
53
+ }
54
+
55
+ logger.info(`Pages: ${project.graph.pages.length}`);
56
+ logger.info(`Output: ${config.deployment.output}`);
57
+ logger.info(`Search: ${config.search.provider}`);
58
+ } catch (error) {
59
+ if (error instanceof BlumeError) {
60
+ diagnostics.push(error.diagnostic);
61
+ } else {
62
+ throw error;
63
+ }
64
+ }
65
+
66
+ const hadErrors = reportDiagnostics(diagnostics, root);
67
+ if (diagnostics.length === 0) {
68
+ logger.success("No problems found.");
69
+ }
70
+ if (hadErrors) {
71
+ process.exit(1);
72
+ }
73
+ },
74
+ });
@@ -0,0 +1,57 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+
3
+ import { defineCommand } from "citty";
4
+ import { join, relative } from "pathe";
5
+
6
+ import { eject } from "../../registry/eject.ts";
7
+ import { logger } from "../log.ts";
8
+
9
+ const updatePackageScripts = async (root: string): Promise<void> => {
10
+ const pkgPath = join(root, "package.json");
11
+ let pkg: Record<string, unknown>;
12
+ try {
13
+ pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
14
+ } catch {
15
+ return;
16
+ }
17
+ const scripts = (pkg.scripts ?? {}) as Record<string, string>;
18
+ pkg.scripts = {
19
+ ...scripts,
20
+ build: "astro build",
21
+ dev: "astro dev",
22
+ preview: "astro preview",
23
+ };
24
+ await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
25
+ };
26
+
27
+ export const ejectCommand = defineCommand({
28
+ args: {
29
+ yes: { description: "Skip the confirmation prompt.", type: "boolean" },
30
+ },
31
+ meta: {
32
+ description: "Promote the generated runtime into an owned Astro project.",
33
+ name: "eject",
34
+ },
35
+ async run({ args }) {
36
+ const root = process.cwd();
37
+
38
+ if (!args.yes) {
39
+ logger.warn(
40
+ "Eject is one-way: it writes astro.config.mjs and src/ into your project and removes .blume."
41
+ );
42
+ logger.info("Re-run with --yes to proceed.");
43
+ return;
44
+ }
45
+
46
+ const files = await eject(root);
47
+ await updatePackageScripts(root);
48
+
49
+ logger.success(`Ejected ${files.length} file(s):`);
50
+ for (const file of files) {
51
+ process.stdout.write(` ${relative(root, file)}\n`);
52
+ }
53
+ logger.box(
54
+ "Your project is now a standalone Astro app.\n\n bun run dev\n bun run build\n\nThe blume package remains importable."
55
+ );
56
+ },
57
+ });
@@ -0,0 +1,98 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+
4
+ import { defineCommand } from "citty";
5
+ import { basename, dirname, join } from "pathe";
6
+
7
+ import { getBlumeVersion } from "../../core/version.ts";
8
+ import { logger } from "../log.ts";
9
+
10
+ /**
11
+ * Derive a valid npm package name from a directory name, falling back to
12
+ * `docs` when nothing usable remains.
13
+ */
14
+ const toPackageName = (raw: string): string =>
15
+ raw
16
+ .toLowerCase()
17
+ .replaceAll(/[^a-z0-9._-]+/gu, "-")
18
+ .replaceAll(/^[-_.]+|[-_.]+$/gu, "") || "docs";
19
+
20
+ const packageTemplate = (name: string, version: string): string => `{
21
+ "name": ${JSON.stringify(name)},
22
+ "private": true,
23
+ "type": "module",
24
+ "scripts": {
25
+ "dev": "blume dev",
26
+ "build": "blume build",
27
+ "doctor": "blume doctor"
28
+ },
29
+ "dependencies": {
30
+ "blume": "^${version}"
31
+ }
32
+ }
33
+ `;
34
+
35
+ const CONFIG_TEMPLATE = `import { defineConfig } from "blume";
36
+
37
+ export default defineConfig({
38
+ title: "My Docs",
39
+ description: "Documentation powered by Blume.",
40
+ });
41
+ `;
42
+
43
+ const INDEX_TEMPLATE = `---
44
+ title: Introduction
45
+ description: Welcome to your new Blume docs.
46
+ ---
47
+
48
+ # Introduction
49
+
50
+ Welcome to **Blume** — markdown-first docs powered by Astro and Vite.
51
+
52
+ Edit \`docs/index.mdx\` to get started, then run \`blume dev\`.
53
+ `;
54
+
55
+ const writeFileSafe = async (
56
+ path: string,
57
+ content: string
58
+ ): Promise<boolean> => {
59
+ if (existsSync(path)) {
60
+ logger.info(`Skipped existing ${path}`);
61
+ return false;
62
+ }
63
+ await mkdir(dirname(path), { recursive: true });
64
+ await writeFile(path, content, "utf-8");
65
+ logger.success(`Created ${path}`);
66
+ return true;
67
+ };
68
+
69
+ export const initCommand = defineCommand({
70
+ args: {
71
+ "content-dir": {
72
+ default: "docs",
73
+ description: "Content directory.",
74
+ type: "string",
75
+ },
76
+ yes: { description: "Skip prompts.", type: "boolean" },
77
+ },
78
+ meta: {
79
+ description: "Scaffold a minimal Blume project.",
80
+ name: "init",
81
+ },
82
+ async run({ args }) {
83
+ const root = process.cwd();
84
+ const contentDir = args["content-dir"] ?? "docs";
85
+
86
+ const createdPackage = await writeFileSafe(
87
+ join(root, "package.json"),
88
+ packageTemplate(toPackageName(basename(root)), getBlumeVersion())
89
+ );
90
+ await writeFileSafe(join(root, "blume.config.ts"), CONFIG_TEMPLATE);
91
+ await writeFileSafe(join(root, contentDir, "index.mdx"), INDEX_TEMPLATE);
92
+
93
+ const nextSteps = createdPackage
94
+ ? "Next steps:\n\n npm install\n blume dev\n"
95
+ : "Next steps:\n\n blume dev\n";
96
+ logger.box(nextSteps);
97
+ },
98
+ });
@@ -0,0 +1,39 @@
1
+ import { defineCommand } from "citty";
2
+
3
+ import { migrators } from "../../migrate/migrate.ts";
4
+ import { logger } from "../log.ts";
5
+
6
+ const makeMigrateCommand = (source: keyof typeof migrators) =>
7
+ defineCommand({
8
+ meta: {
9
+ description: `Migrate a ${source} project to Blume.`,
10
+ name: source,
11
+ },
12
+ async run() {
13
+ const root = process.cwd();
14
+ logger.start(`Migrating ${source} project`);
15
+ const result = await migrators[source]?.(root);
16
+ if (!result) {
17
+ logger.error(`No migrator for ${source}.`);
18
+ process.exit(1);
19
+ }
20
+ logger.success(`Migrated ${result.moved} content file(s).`);
21
+ for (const warning of result.warnings) {
22
+ logger.warn(warning);
23
+ }
24
+ logger.box("Review blume.config.ts and run `blume dev`.");
25
+ },
26
+ });
27
+
28
+ export const migrateCommand = defineCommand({
29
+ meta: {
30
+ description: "Migrate from another docs tool to Blume.",
31
+ name: "migrate",
32
+ },
33
+ subCommands: {
34
+ fumadocs: makeMigrateCommand("fumadocs"),
35
+ mintlify: makeMigrateCommand("mintlify"),
36
+ nextra: makeMigrateCommand("nextra"),
37
+ starlight: makeMigrateCommand("starlight"),
38
+ },
39
+ });
@@ -0,0 +1,39 @@
1
+ import { existsSync } from "node:fs";
2
+
3
+ import { preview } from "astro";
4
+ import { defineCommand } from "citty";
5
+ import { join } from "pathe";
6
+
7
+ import { loadConfig } from "../../core/config.ts";
8
+ import { resolveProjectContext } from "../../core/project.ts";
9
+ import { logger } from "../log.ts";
10
+
11
+ export const previewCommand = defineCommand({
12
+ args: {
13
+ host: { description: "Network host to bind.", type: "string" },
14
+ port: { description: "Port to listen on.", type: "string" },
15
+ },
16
+ meta: {
17
+ description: "Preview the last production build.",
18
+ name: "preview",
19
+ },
20
+ async run({ args }) {
21
+ const root = process.cwd();
22
+ const { config } = await loadConfig(root);
23
+ const context = resolveProjectContext(root, config);
24
+
25
+ if (!existsSync(join(context.outDir, "astro.config.mjs"))) {
26
+ logger.error("No build found. Run `blume build` first.");
27
+ process.exit(1);
28
+ }
29
+
30
+ await preview({
31
+ logLevel: "info",
32
+ root: context.outDir,
33
+ server: {
34
+ host: args.host ?? false,
35
+ port: args.port ? Number(args.port) : undefined,
36
+ },
37
+ });
38
+ },
39
+ });
@@ -0,0 +1,52 @@
1
+ import { rm } from "node:fs/promises";
2
+
3
+ import { defineCommand } from "citty";
4
+ import { join } from "pathe";
5
+
6
+ import { loadConfig } from "../../core/config.ts";
7
+ import { resolveProjectContext } from "../../core/project.ts";
8
+ import { logger } from "../log.ts";
9
+ import { prepareProject } from "../prepare.ts";
10
+
11
+ export const syncCommand = defineCommand({
12
+ args: {
13
+ force: {
14
+ description: "Clear the source cache before refetching.",
15
+ type: "boolean",
16
+ },
17
+ preview: {
18
+ description: "Include drafts and unpublished CMS content.",
19
+ type: "boolean",
20
+ },
21
+ strict: { description: "Fail on diagnostics.", type: "boolean" },
22
+ },
23
+ meta: {
24
+ description: "Re-fetch remote content sources and regenerate the runtime.",
25
+ name: "sync",
26
+ },
27
+ async run({ args }) {
28
+ const root = process.cwd();
29
+
30
+ // `--force` drops the snapshots so a stale or corrupt cache can't be served;
31
+ // scoped to `.blume/cache`, so a running dev server's runtime is untouched.
32
+ if (args.force) {
33
+ const { config } = await loadConfig(root);
34
+ const context = resolveProjectContext(root, config);
35
+ await rm(join(context.outDir, "cache"), { force: true, recursive: true });
36
+ logger.info("Cleared source cache.");
37
+ }
38
+
39
+ // Dev mode keeps drafts and skips the static-build gate; `refresh` forces
40
+ // remote sources to re-fetch rather than serve their cached snapshot. A
41
+ // running dev server hot-reloads from the regenerated runtime.
42
+ await prepareProject({
43
+ mode: "dev",
44
+ preview: args.preview,
45
+ refresh: true,
46
+ root,
47
+ strict: args.strict,
48
+ });
49
+
50
+ logger.success("Synced content sources.");
51
+ },
52
+ });