blume 1.6.5 → 1.7.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 (179) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/bin/blume.mjs +3 -2
  3. package/dist/cli/chunk-0qhq7b8q.js +111 -0
  4. package/dist/cli/chunk-0qhq7b8q.js.map +11 -0
  5. package/dist/cli/chunk-18tjv4f7.js +96 -0
  6. package/dist/cli/chunk-18tjv4f7.js.map +10 -0
  7. package/dist/cli/chunk-27gtm2ym.js +69 -0
  8. package/dist/cli/chunk-27gtm2ym.js.map +11 -0
  9. package/dist/cli/chunk-2aj8ddew.js +72 -0
  10. package/dist/cli/chunk-2aj8ddew.js.map +10 -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-5d4q7121.js +4064 -0
  18. package/dist/cli/chunk-5d4q7121.js.map +40 -0
  19. package/dist/cli/chunk-5hs6gb7n.js +32 -0
  20. package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
  21. package/dist/cli/chunk-6kzzpsx8.js +26 -0
  22. package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
  23. package/dist/cli/chunk-8gnpdsn1.js +952 -0
  24. package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
  25. package/dist/cli/chunk-9qs6acpw.js +176 -0
  26. package/dist/cli/chunk-9qs6acpw.js.map +10 -0
  27. package/dist/cli/chunk-agy5rzxy.js +2453 -0
  28. package/dist/cli/chunk-agy5rzxy.js.map +15 -0
  29. package/dist/cli/chunk-bcy492zc.js +16 -0
  30. package/dist/cli/chunk-bcy492zc.js.map +10 -0
  31. package/dist/cli/chunk-btfr9yvw.js +41 -0
  32. package/dist/cli/chunk-btfr9yvw.js.map +10 -0
  33. package/dist/cli/chunk-cbjnx4s8.js +73 -0
  34. package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
  35. package/dist/cli/chunk-cfw6x4rm.js +1967 -0
  36. package/dist/cli/chunk-cfw6x4rm.js.map +34 -0
  37. package/dist/cli/chunk-ckh3a410.js +277 -0
  38. package/dist/cli/chunk-ckh3a410.js.map +11 -0
  39. package/dist/cli/chunk-drke6t0h.js +259 -0
  40. package/dist/cli/chunk-drke6t0h.js.map +11 -0
  41. package/dist/cli/chunk-ev67ycx0.js +15 -0
  42. package/dist/cli/chunk-ev67ycx0.js.map +10 -0
  43. package/dist/cli/chunk-ey89bjj1.js +209 -0
  44. package/dist/cli/chunk-ey89bjj1.js.map +11 -0
  45. package/dist/cli/chunk-j6pxe0dt.js +69 -0
  46. package/dist/cli/chunk-j6pxe0dt.js.map +11 -0
  47. package/dist/cli/chunk-jk1zwka1.js +387 -0
  48. package/dist/cli/chunk-jk1zwka1.js.map +12 -0
  49. package/dist/cli/chunk-jtb45atp.js +467 -0
  50. package/dist/cli/chunk-jtb45atp.js.map +14 -0
  51. package/dist/cli/chunk-jxkxjsc1.js +76 -0
  52. package/dist/cli/chunk-jxkxjsc1.js.map +10 -0
  53. package/dist/cli/chunk-kwx90v78.js +81 -0
  54. package/dist/cli/chunk-kwx90v78.js.map +10 -0
  55. package/dist/cli/chunk-n0nyat6g.js +30 -0
  56. package/dist/cli/chunk-n0nyat6g.js.map +10 -0
  57. package/dist/cli/chunk-pxj10x8y.js +35 -0
  58. package/dist/cli/chunk-pxj10x8y.js.map +10 -0
  59. package/dist/cli/chunk-qq9nm3qd.js +1141 -0
  60. package/dist/cli/chunk-qq9nm3qd.js.map +19 -0
  61. package/dist/cli/chunk-s102bysw.js +5170 -0
  62. package/dist/cli/chunk-s102bysw.js.map +47 -0
  63. package/dist/cli/chunk-s5dsk8bj.js +769 -0
  64. package/dist/cli/chunk-s5dsk8bj.js.map +13 -0
  65. package/dist/cli/chunk-s5e5jt53.js +227 -0
  66. package/dist/cli/chunk-s5e5jt53.js.map +11 -0
  67. package/dist/cli/chunk-sbdqrjbb.js +81 -0
  68. package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
  69. package/dist/cli/chunk-tnskyrej.js +117 -0
  70. package/dist/cli/chunk-tnskyrej.js.map +10 -0
  71. package/dist/cli/chunk-v2ymm99c.js +1016 -0
  72. package/dist/cli/chunk-v2ymm99c.js.map +13 -0
  73. package/dist/cli/chunk-v5mm027v.js +185 -0
  74. package/dist/cli/chunk-v5mm027v.js.map +11 -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-vxv4x1n8.js +17 -0
  78. package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
  79. package/dist/cli/chunk-wd27zjcz.js +60 -0
  80. package/dist/cli/chunk-wd27zjcz.js.map +10 -0
  81. package/dist/cli/chunk-x66c5yjn.js +23 -0
  82. package/dist/cli/chunk-x66c5yjn.js.map +10 -0
  83. package/dist/cli/chunk-xv91q4nm.js +5314 -0
  84. package/dist/cli/chunk-xv91q4nm.js.map +58 -0
  85. package/dist/cli/chunk-y3g15rvv.js +679 -0
  86. package/dist/cli/chunk-y3g15rvv.js.map +15 -0
  87. package/dist/cli/chunk-ye9zdkgv.js +136 -0
  88. package/dist/cli/chunk-ye9zdkgv.js.map +10 -0
  89. package/dist/cli/chunk-ynacq3ev.js +1062 -0
  90. package/dist/cli/chunk-ynacq3ev.js.map +25 -0
  91. package/dist/cli/chunk-zr3ygrq3.js +54 -0
  92. package/dist/cli/chunk-zr3ygrq3.js.map +10 -0
  93. package/dist/cli/index.js +55 -27597
  94. package/dist/cli/index.js.map +5 -243
  95. package/dist/types/ai/ask-context.d.ts +26 -0
  96. package/dist/types/components/layout/nav-utils.d.ts +33 -1
  97. package/dist/types/core/code-fences.d.ts +11 -0
  98. package/dist/types/core/package-root.d.ts +1 -1
  99. package/dist/types/core/schema.d.ts +70 -0
  100. package/dist/types/theme/fonts.d.ts +22 -22
  101. package/docs/02-deployment.mdx +22 -1
  102. package/docs/configuration/ask-ai.mdx +1 -1
  103. package/docs/configuration/customization.mdx +2 -9
  104. package/docs/content/navigation.mdx +2 -0
  105. package/docs/content/syntax.mdx +1 -1
  106. package/docs/discoverability/open-graph.mdx +4 -0
  107. package/docs/reference/cli.mdx +1 -1
  108. package/package.json +4 -2
  109. package/src/ai/api/handlers.ts +4 -7
  110. package/src/ai/api/paths.ts +8 -0
  111. package/src/ai/api/spec.ts +2 -1
  112. package/src/ai/ask-context.ts +378 -22
  113. package/src/astro/generate.ts +161 -28
  114. package/src/astro/include-hmr.ts +10 -13
  115. package/src/astro/include-refresh.ts +0 -0
  116. package/src/astro/index.ts +6 -1
  117. package/src/astro/integration.ts +280 -53
  118. package/src/astro/module-types.ts +83 -0
  119. package/src/astro/templates.ts +256 -108
  120. package/src/audit/image-size.ts +10 -8
  121. package/src/cli/command-meta.ts +77 -0
  122. package/src/cli/commands/add.ts +2 -4
  123. package/src/cli/commands/audit.ts +2 -4
  124. package/src/cli/commands/build.ts +70 -346
  125. package/src/cli/commands/check.ts +2 -4
  126. package/src/cli/commands/dev.ts +31 -42
  127. package/src/cli/commands/doctor.ts +2 -4
  128. package/src/cli/commands/eject.ts +3 -41
  129. package/src/cli/commands/eval.ts +2 -5
  130. package/src/cli/commands/init.ts +2 -4
  131. package/src/cli/commands/mcp-stdio.ts +2 -5
  132. package/src/cli/commands/preview.ts +3 -5
  133. package/src/cli/commands/sync.ts +2 -4
  134. package/src/cli/commands/translate.ts +2 -5
  135. package/src/cli/commands/validate.ts +2 -4
  136. package/src/cli/commands/version.ts +2 -4
  137. package/src/cli/eject-scripts.ts +0 -45
  138. package/src/cli/host-args.ts +16 -0
  139. package/src/cli/index.ts +84 -35
  140. package/src/cli/lazy-command.ts +47 -0
  141. package/src/components/Icon.astro +24 -0
  142. package/src/components/content/GithubInfo.astro +4 -1
  143. package/src/components/icon-sprite-middleware.ts +41 -0
  144. package/src/components/icon-sprite.ts +93 -0
  145. package/src/components/layout/IconSprite.astro +11 -0
  146. package/src/components/layout/NavTree.astro +156 -188
  147. package/src/components/layout/NavTreeCache.astro +45 -0
  148. package/src/components/layout/NavTreeScript.astro +256 -0
  149. package/src/components/layout/PageActions.astro +11 -5
  150. package/src/components/layout/PageLayout.astro +21 -3
  151. package/src/components/layout/ReferenceLayout.astro +21 -4
  152. package/src/components/layout/RootLayout.astro +44 -6
  153. package/src/components/layout/nav-cache.ts +49 -0
  154. package/src/components/layout/nav-utils.ts +69 -1
  155. package/src/components/layout/page-locale.ts +29 -0
  156. package/src/core/api-name.ts +18 -0
  157. package/src/core/code-fences.ts +48 -0
  158. package/src/core/content-assets.ts +3 -7
  159. package/src/core/includes.ts +3 -7
  160. package/src/core/package-root.ts +1 -1
  161. package/src/core/schema.ts +19 -0
  162. package/src/core/sources/normalize.ts +2 -37
  163. package/src/core/sources/obsidian.ts +3 -2
  164. package/src/core/svg-dimensions.ts +97 -0
  165. package/src/core/version-cut.ts +2 -2
  166. package/src/deploy/artifacts.ts +370 -0
  167. package/src/deploy/cloudflare-negotiation.ts +97 -32
  168. package/src/deploy/function-bundle.ts +66 -20
  169. package/src/deploy/sitemap.ts +6 -0
  170. package/src/deploy/vercel-negotiation.ts +8 -30
  171. package/src/markdown/language-icon.ts +64 -20
  172. package/src/markdown/mermaid.ts +11 -0
  173. package/src/og/cache.ts +236 -0
  174. package/src/og/card.ts +18 -16
  175. package/src/og/index.ts +8 -1
  176. package/src/openapi/render-mdx.ts +9 -5
  177. package/src/registry/eject.ts +23 -10
  178. package/src/theme/entry.ts +41 -7
  179. package/src/theme/fonts.ts +30 -23
@@ -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[] = [];
@@ -1,63 +1,26 @@
1
1
  import { defineCommand } from "citty";
2
2
  import { relative } from "pathe";
3
3
 
4
- import { loadConfig } from "../../core/config.ts";
5
4
  import { eject } from "../../registry/eject.ts";
5
+ import { commandMeta } from "../command-meta.ts";
6
6
  import { refuseIfDevRunning } from "../dev-lock.ts";
7
- import {
8
- droppedArtifactNotices,
9
- updatePackageScripts,
10
- } from "../eject-scripts.ts";
7
+ import { updatePackageScripts } from "../eject-scripts.ts";
11
8
  import { commandsFor, detectProjectPackageManager } from "../init/scaffold.ts";
12
9
  import { logger } from "../log.ts";
13
10
 
14
- /**
15
- * Warn which `blume build` post-build artifacts the ejected app stops
16
- * producing. Printed both at the confirmation (so the decision is informed)
17
- * and after `--yes` (so a direct eject still sees it). No-op when the config
18
- * activates none of them.
19
- */
20
- const reportDroppedArtifacts = (notices: string[]): void => {
21
- if (notices.length === 0) {
22
- return;
23
- }
24
- logger.warn(
25
- [
26
- "The ejected build script runs plain `astro build`, which stops producing these `blume build` artifacts:",
27
- ...notices.map((notice) => ` - ${notice}`),
28
- ].join("\n")
29
- );
30
- };
31
-
32
11
  export const ejectCommand = defineCommand({
33
12
  args: {
34
13
  yes: { description: "Skip the confirmation prompt.", type: "boolean" },
35
14
  },
36
- meta: {
37
- description: "Promote the generated runtime into an owned Astro project.",
38
- name: "eject",
39
- },
15
+ meta: commandMeta.eject,
40
16
  async run({ args }) {
41
17
  const root = process.cwd();
42
18
  refuseIfDevRunning(root, "ejecting");
43
19
 
44
- // Config-aware drop list: only the artifacts this project actually
45
- // produces are mentioned (e.g. the Pagefind index only for
46
- // `search.provider: "pagefind"`).
47
- let notices: string[] = [];
48
- try {
49
- const { config } = await loadConfig(root);
50
- notices = droppedArtifactNotices(config);
51
- } catch {
52
- // A config that fails to load can't gate the notice; the eject itself
53
- // surfaces the load error.
54
- }
55
-
56
20
  if (!args.yes) {
57
21
  logger.warn(
58
22
  "Eject is one-way: it writes astro.config.mjs, src/, and (if absent) tsconfig.json, rewrites your package.json scripts, and removes .blume. An existing tsconfig.json is left untouched."
59
23
  );
60
- reportDroppedArtifacts(notices);
61
24
  logger.info("Re-run with --yes to proceed.");
62
25
  return;
63
26
  }
@@ -75,7 +38,6 @@ export const ejectCommand = defineCommand({
75
38
  for (const file of files) {
76
39
  process.stdout.write(` ${relative(root, file)}\n`);
77
40
  }
78
- reportDroppedArtifacts(notices);
79
41
  // Print run commands matching the project's package manager (lockfile
80
42
  // detection, since eject runs inside an existing project).
81
43
  const pm = await detectProjectPackageManager(root);
@@ -22,6 +22,7 @@ import {
22
22
  import { runEval } from "../../eval/run.ts";
23
23
  import type { EvalResult } from "../../eval/run.ts";
24
24
  import { EvalsFileError, loadEvalsFile } from "../../eval/schema.ts";
25
+ import { commandMeta } from "../command-meta.ts";
25
26
  import { reportInternalError } from "../internal-error.ts";
26
27
  import { flushStdout, logger } from "../log.ts";
27
28
 
@@ -183,11 +184,7 @@ export const evalCommand = defineCommand({
183
184
  type: "boolean",
184
185
  },
185
186
  },
186
- meta: {
187
- description:
188
- "Test the docs: an agent answers your questions using only the documentation.",
189
- name: "eval",
190
- },
187
+ meta: commandMeta.eval,
191
188
  async run({ args }) {
192
189
  const root = process.cwd();
193
190
  const { agent, threshold, timeoutS } = parseFlags(args);
@@ -4,6 +4,7 @@ import { resolve } from "pathe";
4
4
 
5
5
  import { ensureGitignore } from "../../core/gitignore.ts";
6
6
  import { eject } from "../../registry/eject.ts";
7
+ import { commandMeta } from "../command-meta.ts";
7
8
  import { updatePackageScripts } from "../eject-scripts.ts";
8
9
  import { collectAnswers } from "../init/questions.ts";
9
10
  import {
@@ -84,10 +85,7 @@ export const initCommand = defineCommand({
84
85
  type: "boolean",
85
86
  },
86
87
  },
87
- meta: {
88
- description: "Scaffold a minimal Blume project.",
89
- name: "init",
90
- },
88
+ meta: commandMeta.init,
91
89
  async run({ args }) {
92
90
  const cwd = process.cwd();
93
91
 
@@ -4,6 +4,7 @@ import { defineCommand } from "citty";
4
4
 
5
5
  import type { McpData } from "../../ai/mcp/data.ts";
6
6
  import { serveMcpStdio } from "../../ai/mcp/stdio.ts";
7
+ import { commandMeta } from "../command-meta.ts";
7
8
 
8
9
  export const mcpStdioCommand = defineCommand({
9
10
  args: {
@@ -13,11 +14,7 @@ export const mcpStdioCommand = defineCommand({
13
14
  type: "string",
14
15
  },
15
16
  },
16
- meta: {
17
- description:
18
- "Serve an MCP data snapshot over stdio (internal, used by `blume eval`).",
19
- name: "mcp-stdio",
20
- },
17
+ meta: commandMeta["mcp-stdio"],
21
18
  async run({ args }) {
22
19
  // stdout belongs to the JSON-RPC transport from here on; every diagnostic
23
20
  // must go to stderr or the MCP client chokes on the stray line.
@@ -7,18 +7,16 @@ import { join } from "pathe";
7
7
  import { loadConfig } from "../../core/config.ts";
8
8
  import { resolveProjectContext } from "../../core/project.ts";
9
9
  import { parsePort } from "../args.ts";
10
+ import { commandMeta } from "../command-meta.ts";
11
+ import { normalizeHost } from "../host-args.ts";
10
12
  import { logger } from "../log.ts";
11
- import { normalizeHost } from "./dev.ts";
12
13
 
13
14
  export const previewCommand = defineCommand({
14
15
  args: {
15
16
  host: { description: "Network host to bind.", type: "string" },
16
17
  port: { description: "Port to listen on.", type: "string" },
17
18
  },
18
- meta: {
19
- description: "Preview the last production build.",
20
- name: "preview",
21
- },
19
+ meta: commandMeta.preview,
22
20
  async run({ args }) {
23
21
  const root = process.cwd();
24
22
  const { config } = await loadConfig(root);
@@ -8,6 +8,7 @@ import {
8
8
  resolveProjectContext,
9
9
  resolveRuntimeDir,
10
10
  } from "../../core/project.ts";
11
+ import { commandMeta } from "../command-meta.ts";
11
12
  import { readDevLock } from "../dev-lock.ts";
12
13
  import { logger } from "../log.ts";
13
14
  import { prepareProject } from "../prepare.ts";
@@ -24,10 +25,7 @@ export const syncCommand = defineCommand({
24
25
  },
25
26
  strict: { description: "Fail on diagnostics.", type: "boolean" },
26
27
  },
27
- meta: {
28
- description: "Re-fetch remote content sources and regenerate the runtime.",
29
- name: "sync",
30
- },
28
+ meta: commandMeta.sync,
31
29
  async run({ args }) {
32
30
  const root = process.cwd();
33
31
 
@@ -26,6 +26,7 @@ import {
26
26
  } from "../../translate/report.ts";
27
27
  import { runTranslate } from "../../translate/run.ts";
28
28
  import { computeWorkList } from "../../translate/work-list.ts";
29
+ import { commandMeta } from "../command-meta.ts";
29
30
  import { reportInternalError } from "../internal-error.ts";
30
31
  import { flushStdout, logger } from "../log.ts";
31
32
 
@@ -181,11 +182,7 @@ export const translateCommand = defineCommand({
181
182
  type: "string",
182
183
  },
183
184
  },
184
- meta: {
185
- description:
186
- "Translate docs into the configured locales with a local agent CLI.",
187
- name: "translate",
188
- },
185
+ meta: commandMeta.translate,
189
186
  async run({ args }) {
190
187
  const root = process.cwd();
191
188
  const { agent, concurrency, timeoutS } = parseFlags(args);
@@ -13,6 +13,7 @@ import { validateLinks } from "../../core/links.ts";
13
13
  import { buildManifest } from "../../core/manifest.ts";
14
14
  import { scanProject } from "../../core/project-graph.ts";
15
15
  import type { Diagnostic } from "../../core/types.ts";
16
+ import { commandMeta } from "../command-meta.ts";
16
17
  import { reportInternalError } from "../internal-error.ts";
17
18
  import {
18
19
  flushStdout,
@@ -36,10 +37,7 @@ export const validateCommand = defineCommand({
36
37
  type: "boolean",
37
38
  },
38
39
  },
39
- meta: {
40
- description: "Validate internal, anchor, asset, and external links.",
41
- name: "validate",
42
- },
40
+ meta: commandMeta.validate,
43
41
  async run({ args }) {
44
42
  const root = process.cwd();
45
43
  const diagnostics: Diagnostic[] = [];
@@ -3,6 +3,7 @@ import { defineCommand } from "citty";
3
3
  import { loadConfig } from "../../core/config.ts";
4
4
  import { BlumeError } from "../../core/diagnostics.ts";
5
5
  import { CutError, cutVersion } from "../../core/version-cut.ts";
6
+ import { commandMeta } from "../command-meta.ts";
6
7
  import { reportInternalError } from "../internal-error.ts";
7
8
  import { logger } from "../log.ts";
8
9
 
@@ -18,10 +19,7 @@ export const versionCommand = defineCommand({
18
19
  type: "positional",
19
20
  },
20
21
  },
21
- meta: {
22
- description: "Freeze the current docs as an archived version.",
23
- name: "version",
24
- },
22
+ meta: commandMeta.version,
25
23
  async run({ args }) {
26
24
  const root = process.cwd();
27
25
 
@@ -2,9 +2,6 @@ import { readFile, writeFile } from "node:fs/promises";
2
2
 
3
3
  import { join } from "pathe";
4
4
 
5
- import type { ResolvedConfig } from "../core/schema.ts";
6
- import { searchProviderMeta } from "../search/providers.ts";
7
-
8
5
  /** A JSON value, as `JSON.parse` of a manifest can return. */
9
6
  type JsonValue =
10
7
  | string
@@ -20,48 +17,6 @@ interface PackageManifest {
20
17
  scripts?: Record<string, string>;
21
18
  }
22
19
 
23
- /**
24
- * The `blume build`-only artifacts this project's config actually produces, as
25
- * notice lines for the eject command. After an eject the build script runs
26
- * plain `astro build`, which emits none of them — printing the config-aware
27
- * list makes the loss explicit instead of silent (a Pagefind site would
28
- * otherwise eject into a build whose search fails at runtime). Each gate
29
- * mirrors the artifact's producer in `blume build` (cli/commands/build.ts).
30
- */
31
- export const droppedArtifactNotices = (config: ResolvedConfig): string[] => {
32
- const notices: string[] = [];
33
- if (config.search.provider === "pagefind") {
34
- notices.push(
35
- 'the Pagefind search index — the search UI loads it from the built site, so search will break in production. Add a post-build step: `"build": "astro build && pagefind --site dist"` (with `pagefind` installed as a devDependency).'
36
- );
37
- }
38
- if (searchProviderMeta(config.search.provider).syncs) {
39
- notices.push(
40
- `the hosted ${config.search.provider} index sync — new and updated pages stop being pushed; re-upload your search records after each build with the provider's API or CLI.`
41
- );
42
- }
43
- if (config.ai.llmsTxt.enabled) {
44
- notices.push("llms.txt and llms-full.txt");
45
- }
46
- if (config.deployment.site && config.seo.sitemap) {
47
- notices.push(
48
- "sitemap.xml — recreate it with the @astrojs/sitemap integration."
49
- );
50
- }
51
- if (config.seo.robots) {
52
- notices.push("robots.txt — recreate it as a public/robots.txt file.");
53
- }
54
- if (config.seo.agentReadability) {
55
- notices.push("agent-readability.json");
56
- }
57
- if (config.redirects.length > 0 && config.deployment.output === "static") {
58
- notices.push(
59
- "the platform redirect files (_redirects, vercel.json) — your redirects still work as Astro-generated meta-refresh pages."
60
- );
61
- }
62
- return notices;
63
- };
64
-
65
20
  /**
66
21
  * Rewrite the project's package.json scripts to run Astro directly. After an
67
22
  * eject the Blume CLI no longer manages the runtime, so scaffolded scripts like
@@ -16,3 +16,19 @@ export const normalizeHostArgs = (rawArgs: readonly string[]): string[] =>
16
16
  const next = rawArgs[index + 1];
17
17
  return next === undefined || next.startsWith("-") ? "--host=" : arg;
18
18
  });
19
+
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 `""` (`normalizeHostArgs` rewrites it
24
+ * to `--host=` first) — Node would bind all interfaces for `""`, but Vite's
25
+ * `resolveHostname` treats it as a literal hostname and prints malformed URLs
26
+ * like `http://:4321/`. Match Astro's own `--host` semantics instead: bare
27
+ * flag → `true` (bind all interfaces), `--host 10.0.0.1` → that address,
28
+ * absent → `false` (localhost only).
29
+ *
30
+ * Lives beside `normalizeHostArgs` rather than in `commands/dev.ts` so
31
+ * `preview` can share it without importing the whole dev command graph.
32
+ */
33
+ export const normalizeHost = (host?: string): boolean | string =>
34
+ host === "" ? true : (host ?? false);
package/src/cli/index.ts CHANGED
@@ -1,24 +1,18 @@
1
1
  import { defineCommand, runMain } from "citty";
2
2
 
3
3
  import { getBlumeVersion } from "../core/version.ts";
4
- import { addCommand } from "./commands/add.ts";
5
- import { auditCommand } from "./commands/audit.ts";
6
- import { buildCommand } from "./commands/build.ts";
7
- import { checkCommand } from "./commands/check.ts";
8
- import { devCommand } from "./commands/dev.ts";
9
- import { doctorCommand } from "./commands/doctor.ts";
10
- import { ejectCommand } from "./commands/eject.ts";
11
- import { evalCommand } from "./commands/eval.ts";
12
- import { initCommand } from "./commands/init.ts";
13
- import { mcpStdioCommand } from "./commands/mcp-stdio.ts";
14
- import { previewCommand } from "./commands/preview.ts";
15
- import { syncCommand } from "./commands/sync.ts";
16
- import { translateCommand } from "./commands/translate.ts";
17
- import { validateCommand } from "./commands/validate.ts";
18
- import { versionCommand } from "./commands/version.ts";
4
+ import { commandMeta } from "./command-meta.ts";
19
5
  import { loadEnvFiles } from "./env.ts";
20
6
  import { normalizeHostArgs } from "./host-args.ts";
21
7
  import { reportInternalError } from "./internal-error.ts";
8
+ import { lazyCommand } from "./lazy-command.ts";
9
+ // consola reads CONSOLA_LEVEL / NODE_ENV / TEST / CI / DEBUG when its module
10
+ // evaluates. Import the logger here, ahead of `loadEnvFiles`, so those values
11
+ // come from the real environment — never from a project `.env`, which would
12
+ // otherwise silence or restyle every command's output. The commands are loaded
13
+ // lazily below, so without this line consola would first evaluate after `.env`
14
+ // had been applied.
15
+ import "./log.ts";
22
16
 
23
17
  const main = defineCommand({
24
18
  meta: {
@@ -27,31 +21,86 @@ const main = defineCommand({
27
21
  version: getBlumeVersion(),
28
22
  },
29
23
  subCommands: {
30
- add: addCommand,
31
- audit: auditCommand,
32
- build: buildCommand,
33
- check: checkCommand,
34
- dev: devCommand,
35
- doctor: doctorCommand,
36
- eject: ejectCommand,
37
- eval: evalCommand,
38
- init: initCommand,
39
- "mcp-stdio": mcpStdioCommand,
40
- preview: previewCommand,
41
- sync: syncCommand,
42
- translate: translateCommand,
43
- validate: validateCommand,
44
- version: versionCommand,
24
+ add: lazyCommand(
25
+ commandMeta.add,
26
+ () => import("./commands/add.ts"),
27
+ "addCommand"
28
+ ),
29
+ audit: lazyCommand(
30
+ commandMeta.audit,
31
+ () => import("./commands/audit.ts"),
32
+ "auditCommand"
33
+ ),
34
+ build: lazyCommand(
35
+ commandMeta.build,
36
+ () => import("./commands/build.ts"),
37
+ "buildCommand"
38
+ ),
39
+ check: lazyCommand(
40
+ commandMeta.check,
41
+ () => import("./commands/check.ts"),
42
+ "checkCommand"
43
+ ),
44
+ dev: lazyCommand(
45
+ commandMeta.dev,
46
+ () => import("./commands/dev.ts"),
47
+ "devCommand"
48
+ ),
49
+ doctor: lazyCommand(
50
+ commandMeta.doctor,
51
+ () => import("./commands/doctor.ts"),
52
+ "doctorCommand"
53
+ ),
54
+ eject: lazyCommand(
55
+ commandMeta.eject,
56
+ () => import("./commands/eject.ts"),
57
+ "ejectCommand"
58
+ ),
59
+ eval: lazyCommand(
60
+ commandMeta.eval,
61
+ () => import("./commands/eval.ts"),
62
+ "evalCommand"
63
+ ),
64
+ init: lazyCommand(
65
+ commandMeta.init,
66
+ () => import("./commands/init.ts"),
67
+ "initCommand"
68
+ ),
69
+ "mcp-stdio": lazyCommand(
70
+ commandMeta["mcp-stdio"],
71
+ () => import("./commands/mcp-stdio.ts"),
72
+ "mcpStdioCommand"
73
+ ),
74
+ preview: lazyCommand(
75
+ commandMeta.preview,
76
+ () => import("./commands/preview.ts"),
77
+ "previewCommand"
78
+ ),
79
+ sync: lazyCommand(
80
+ commandMeta.sync,
81
+ () => import("./commands/sync.ts"),
82
+ "syncCommand"
83
+ ),
84
+ translate: lazyCommand(
85
+ commandMeta.translate,
86
+ () => import("./commands/translate.ts"),
87
+ "translateCommand"
88
+ ),
89
+ validate: lazyCommand(
90
+ commandMeta.validate,
91
+ () => import("./commands/validate.ts"),
92
+ "validateCommand"
93
+ ),
94
+ version: lazyCommand(
95
+ commandMeta.version,
96
+ () => import("./commands/version.ts"),
97
+ "versionCommand"
98
+ ),
45
99
  },
46
100
  });
47
101
 
48
- // Load `.env`/`.env.local` before any command runs so remote content sources
49
- // can read their tokens (e.g. `GITHUB_TOKEN`) during the content scan.
50
102
  loadEnvFiles(process.cwd());
51
103
 
52
- // Backstop for unexpected async failures that escape a command's own handling
53
- // (e.g. a rejected timer/watcher in `blume dev`), so even those report through
54
- // the stable internal-error contract rather than a bare stack trace.
55
104
  process.on("uncaughtException", (error) => {
56
105
  reportInternalError(error);
57
106
  process.exit(1);
@@ -0,0 +1,47 @@
1
+ import type { ArgsDef, CommandDef, CommandMeta } from "citty";
2
+
3
+ /**
4
+ * Wrap a command so its module is imported only when citty needs its `args`
5
+ * or runs it. `meta` stays static (from `command-meta.ts`), so rendering root
6
+ * usage, matching an unknown name, and `blume <cmd> --help` for *another*
7
+ * command never touch this one's dependency graph.
8
+ *
9
+ * `load` imports the command's module and `key` names its export, so the call
10
+ * site stays a plain `() => import("./commands/x.ts")`.
11
+ *
12
+ * Only `args`, `setup`, `run`, and `cleanup` are forwarded: no Blume command
13
+ * declares `default`, `plugins`, or nested `subCommands`.
14
+ */
15
+ export const lazyCommand = <Args extends ArgsDef, Key extends string>(
16
+ meta: CommandMeta,
17
+ load: () => Promise<Record<Key, CommandDef<Args>>>,
18
+ key: Key
19
+ ): CommandDef<Args> => {
20
+ const command = async () => {
21
+ const module = await load();
22
+ return module[key];
23
+ };
24
+ return {
25
+ args: async () => {
26
+ const { args } = await command();
27
+ // SAFETY: every Blume command passes `args` to `defineCommand` as a plain
28
+ // object literal (the "command registry" test checks it); citty's
29
+ // `Resolvable` widening is the only reason the type also admits a thunk
30
+ // or a promise here.
31
+ return args as Args;
32
+ },
33
+ cleanup: async (context) => {
34
+ const loaded = await command();
35
+ await loaded.cleanup?.(context);
36
+ },
37
+ meta,
38
+ run: async (context) => {
39
+ const loaded = await command();
40
+ await loaded.run?.(context);
41
+ },
42
+ setup: async (context) => {
43
+ const loaded = await command();
44
+ await loaded.setup?.(context);
45
+ },
46
+ };
47
+ };