blume 0.7.0 → 0.8.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 (168) hide show
  1. package/CHANGELOG.md +618 -0
  2. package/LICENSE +21 -0
  3. package/README.md +107 -0
  4. package/dist/cli/index.js +1487 -360
  5. package/dist/cli/index.js.map +91 -85
  6. package/dist/types/ai/component-markdown.d.ts +34 -0
  7. package/dist/types/components/content/youtube.d.ts +18 -0
  8. package/dist/types/core/base-path.d.ts +9 -0
  9. package/dist/types/core/config-input.d.ts +36 -2
  10. package/dist/types/core/config.d.ts +3 -2
  11. package/dist/types/core/data.d.ts +2 -0
  12. package/dist/types/core/i18n-ui.d.ts +476 -132
  13. package/dist/types/core/schema.d.ts +216 -145
  14. package/dist/types/index.d.ts +1 -0
  15. package/dist/types/openapi/references.d.ts +60 -0
  16. package/docs/01-quickstart.mdx +5 -2
  17. package/docs/02-deployment.mdx +8 -8
  18. package/docs/03-faq.mdx +46 -16
  19. package/docs/advanced/custom-pages.mdx +1 -1
  20. package/docs/advanced/skills.mdx +1 -1
  21. package/docs/configuration/ai.mdx +49 -10
  22. package/docs/configuration/customization.mdx +11 -0
  23. package/docs/configuration/index.mdx +27 -3
  24. package/docs/configuration/seo.mdx +2 -2
  25. package/docs/content/components.mdx +1 -1
  26. package/docs/content/i18n.mdx +1 -1
  27. package/docs/content/navigation.mdx +3 -3
  28. package/docs/content/sources.mdx +1 -1
  29. package/docs/content/syntax.mdx +4 -2
  30. package/docs/index.mdx +2 -2
  31. package/docs/reference/cli.mdx +8 -6
  32. package/package.json +14 -4
  33. package/skills/blume/SKILL.md +5 -3
  34. package/skills/blume-update-docs/SKILL.md +3 -2
  35. package/src/ai/agent-readability.ts +9 -8
  36. package/src/ai/ask-context.ts +7 -2
  37. package/src/ai/ask-data.ts +3 -0
  38. package/src/ai/component-markdown.ts +461 -0
  39. package/src/ai/llms.ts +135 -26
  40. package/src/ai/markdown.ts +35 -6
  41. package/src/ai/mcp/data.ts +25 -4
  42. package/src/ai/mcp/discovery.ts +10 -3
  43. package/src/ai/mcp/server.ts +21 -7
  44. package/src/ai/visibility.ts +74 -0
  45. package/src/astro/component-slots.ts +11 -1
  46. package/src/astro/generate.ts +76 -45
  47. package/src/astro/integration.ts +1 -1
  48. package/src/astro/markdown-negotiation.ts +1 -1
  49. package/src/astro/pages.ts +81 -19
  50. package/src/astro/templates.ts +99 -12
  51. package/src/blume-modules.d.ts +8 -0
  52. package/src/cli/commands/build.ts +99 -19
  53. package/src/cli/commands/check.ts +1 -1
  54. package/src/cli/commands/dev.ts +26 -5
  55. package/src/cli/commands/eject.ts +47 -19
  56. package/src/cli/commands/init.ts +120 -180
  57. package/src/cli/commands/preview.ts +4 -1
  58. package/src/cli/commands/validate.ts +43 -2
  59. package/src/cli/dev-lock.ts +8 -4
  60. package/src/cli/eject-scripts.ts +72 -0
  61. package/src/cli/env.ts +15 -5
  62. package/src/cli/init/questions.ts +158 -0
  63. package/src/cli/init/scaffold.ts +380 -0
  64. package/src/components/content/AccordionItem.astro +23 -4
  65. package/src/components/content/Badge.astro +3 -1
  66. package/src/components/content/Card.astro +4 -2
  67. package/src/components/content/Step.astro +10 -1
  68. package/src/components/content/Tabs.astro +15 -3
  69. package/src/components/content/Tile.astro +2 -1
  70. package/src/components/content/Tooltip.astro +3 -1
  71. package/src/components/content/Update.astro +9 -2
  72. package/src/components/content/auto-type-table.ts +7 -1
  73. package/src/components/content/base-href.ts +33 -0
  74. package/src/components/content/changelog-element.ts +9 -2
  75. package/src/components/content/mermaid-element.ts +7 -2
  76. package/src/components/islands/AskAI.astro +5 -2
  77. package/src/components/islands/ask-ai.tsx +56 -6
  78. package/src/components/islands/hooks.ts +28 -8
  79. package/src/components/layout/Banner.astro +10 -2
  80. package/src/components/layout/Header.astro +13 -4
  81. package/src/components/layout/Logo.astro +11 -3
  82. package/src/components/layout/NavTree.astro +17 -3
  83. package/src/components/layout/PageActions.astro +25 -10
  84. package/src/components/layout/PageLayout.astro +45 -8
  85. package/src/components/layout/ReferenceLayout.astro +8 -1
  86. package/src/components/layout/RootLayout.astro +67 -9
  87. package/src/components/layout/Search.astro +94 -22
  88. package/src/components/layout/search/algolia.ts +11 -2
  89. package/src/components/layout/search/endpoint.ts +11 -5
  90. package/src/components/layout/search/orama-cloud.ts +8 -2
  91. package/src/components/layout/search/types.ts +5 -1
  92. package/src/components/layout/search/typesense.ts +4 -1
  93. package/src/components/layout/toc-element.ts +1 -1
  94. package/src/components/openapi/ApiTagOperations.astro +2 -1
  95. package/src/components/openapi/Operation.astro +47 -40
  96. package/src/components/openapi/RequestPanel.astro +1 -1
  97. package/src/components/openapi/helpers.ts +71 -3
  98. package/src/components/openapi/panel.ts +1 -1
  99. package/src/core/base-path.ts +24 -0
  100. package/src/core/builtin-tags.ts +2 -0
  101. package/src/core/config-input.ts +37 -2
  102. package/src/core/config.ts +3 -2
  103. package/src/core/data.ts +2 -0
  104. package/src/core/graph.ts +15 -5
  105. package/src/core/i18n-ui.ts +45 -0
  106. package/src/core/last-modified.ts +13 -6
  107. package/src/core/links.ts +32 -8
  108. package/src/core/navigation.ts +29 -4
  109. package/src/core/package-json.ts +17 -2
  110. package/src/core/project-graph.ts +15 -6
  111. package/src/core/schema.ts +36 -2
  112. package/src/core/sources/assets.ts +6 -1
  113. package/src/core/sources/filesystem.ts +4 -0
  114. package/src/core/sources/mdx-remote.ts +23 -14
  115. package/src/core/sources/normalize.ts +152 -50
  116. package/src/core/sources/notion.ts +8 -8
  117. package/src/core/ui-packs/ar.ts +1 -0
  118. package/src/core/ui-packs/bg.ts +1 -0
  119. package/src/core/ui-packs/bn.ts +1 -0
  120. package/src/core/ui-packs/ca.ts +1 -0
  121. package/src/core/ui-packs/cs.ts +1 -0
  122. package/src/core/ui-packs/da.ts +1 -0
  123. package/src/core/ui-packs/de.ts +1 -0
  124. package/src/core/ui-packs/el.ts +1 -0
  125. package/src/core/ui-packs/es.ts +1 -0
  126. package/src/core/ui-packs/fa.ts +1 -0
  127. package/src/core/ui-packs/fi.ts +1 -0
  128. package/src/core/ui-packs/fr.ts +2 -1
  129. package/src/core/ui-packs/he.ts +1 -0
  130. package/src/core/ui-packs/hi.ts +1 -0
  131. package/src/core/ui-packs/hr.ts +1 -0
  132. package/src/core/ui-packs/hu.ts +1 -0
  133. package/src/core/ui-packs/id.ts +1 -0
  134. package/src/core/ui-packs/it.ts +1 -0
  135. package/src/core/ui-packs/ja.ts +1 -0
  136. package/src/core/ui-packs/ko.ts +1 -0
  137. package/src/core/ui-packs/nl.ts +1 -0
  138. package/src/core/ui-packs/no.ts +1 -0
  139. package/src/core/ui-packs/pl.ts +1 -0
  140. package/src/core/ui-packs/pt-br.ts +1 -0
  141. package/src/core/ui-packs/pt.ts +1 -0
  142. package/src/core/ui-packs/ro.ts +1 -0
  143. package/src/core/ui-packs/ru.ts +1 -0
  144. package/src/core/ui-packs/sk.ts +1 -0
  145. package/src/core/ui-packs/sr.ts +1 -0
  146. package/src/core/ui-packs/sv.ts +1 -0
  147. package/src/core/ui-packs/th.ts +1 -0
  148. package/src/core/ui-packs/tr.ts +1 -0
  149. package/src/core/ui-packs/uk.ts +1 -0
  150. package/src/core/ui-packs/vi.ts +1 -0
  151. package/src/core/ui-packs/zh-tw.ts +1 -0
  152. package/src/core/ui-packs/zh.ts +1 -0
  153. package/src/deploy/adapter-output.ts +18 -8
  154. package/src/deploy/redirects.ts +7 -2
  155. package/src/deploy/sitemap.ts +53 -11
  156. package/src/index.ts +5 -0
  157. package/src/markdown/base-links.ts +10 -8
  158. package/src/markdown/index.ts +15 -3
  159. package/src/markdown/inline-code.ts +7 -2
  160. package/src/markdown/package-commands.ts +10 -4
  161. package/src/openapi/model.ts +12 -4
  162. package/src/openapi/parse.ts +21 -0
  163. package/src/openapi/references.ts +38 -8
  164. package/src/openapi/source.ts +59 -10
  165. package/src/registry/eject.ts +184 -12
  166. package/src/registry/registry.ts +0 -3
  167. package/src/search/documents.ts +34 -2
  168. package/src/seo/jsonld.ts +13 -12
@@ -11,6 +11,7 @@ import { ensureGitignore } from "../../core/gitignore.ts";
11
11
  import type { BlumeProject } from "../../core/project-graph.ts";
12
12
  import type { ResolvedConfig } from "../../core/schema.ts";
13
13
  import { serverFeatures } from "../../core/server-features.ts";
14
+ import type { ProjectContext } from "../../core/types.ts";
14
15
  import {
15
16
  deployStaticDir,
16
17
  surfaceAdapterOutput,
@@ -189,11 +190,89 @@ const enforceBudget = async (
189
190
  return passed ? "pass" : "fail";
190
191
  };
191
192
 
193
+ /**
194
+ * Run the optional bundle report (`--analyze`) and performance-budget gate
195
+ * against the directory whose `_astro/` client assets the deploy serves.
196
+ * Shared by real and isolated builds — an isolated CI run passing
197
+ * `--budget-js` must still fail on an exceeded budget rather than silently
198
+ * skipping the check. Exits non-zero when a budget is exceeded.
199
+ */
200
+ export const runClientAssetChecks = async (
201
+ staticDir: string,
202
+ args: { analyze?: boolean } & BudgetArgs
203
+ ): Promise<void> => {
204
+ if (args.analyze) {
205
+ await reportBundleSizes(staticDir);
206
+ }
207
+ if ((await enforceBudget(staticDir, args)) === "fail") {
208
+ process.exit(1);
209
+ }
210
+ };
211
+
212
+ /**
213
+ * Directory holding an isolated build's client `_astro/` assets. Mirrors
214
+ * `deployStaticDir`, except that an isolated build never surfaces the adapter
215
+ * bundle to the project root — a Vercel server build's static output stays at
216
+ * `<runtime>/.vercel/output/static`, where `deployStaticDir` would instead
217
+ * point at the project-root copy (a previous real build's assets, or nothing).
218
+ */
219
+ export const isolatedStaticDir = (
220
+ config: ResolvedConfig,
221
+ context: ProjectContext
222
+ ): string => {
223
+ const { adapter, output } = config.deployment;
224
+ if (output === "server" && adapter === "vercel") {
225
+ return join(context.outDir, ".vercel", "output", "static");
226
+ }
227
+ const dist = context.distDir ?? join(context.outDir, "dist");
228
+ if (output === "server" && adapter === "node") {
229
+ return join(dist, "client");
230
+ }
231
+ return dist;
232
+ };
233
+
234
+ /**
235
+ * Generate `llms.txt`/`llms-full.txt` into the dist dir. A user's own file in
236
+ * `public/` (copied into dist by Astro before this runs, like the sitemap and
237
+ * robots.txt) wins over the generated one — each file is checked and replaced
238
+ * independently, so a custom `llms.txt` still gets a generated `llms-full.txt`.
239
+ */
240
+ const publishLlmsFiles = async (
241
+ project: BlumeProject,
242
+ distDir: string
243
+ ): Promise<void> => {
244
+ const indexPath = join(distDir, "llms.txt");
245
+ const fullPath = join(distDir, "llms-full.txt");
246
+ const writeIndex = !existsSync(indexPath);
247
+ const writeFull = !existsSync(fullPath);
248
+ if (!(writeIndex || writeFull)) {
249
+ return;
250
+ }
251
+ const { index, full } = await buildLlmsFiles(project);
252
+ const writes: Promise<void>[] = [];
253
+ if (writeIndex) {
254
+ writes.push(writeFile(indexPath, index, "utf-8"));
255
+ }
256
+ if (writeFull) {
257
+ writes.push(writeFile(fullPath, full, "utf-8"));
258
+ }
259
+ await Promise.all(writes);
260
+ logger.success(
261
+ `Generated ${[
262
+ writeIndex ? "llms.txt" : null,
263
+ writeFull ? "llms-full.txt" : null,
264
+ ]
265
+ .filter(Boolean)
266
+ .join(" and ")}`
267
+ );
268
+ };
269
+
192
270
  /**
193
271
  * Run every deploy post-step of a real (non-isolated) build: the search index +
194
272
  * hosted-provider sync, llms.txt, sitemap/robots, redirect files, the summary
195
273
  * box, and the optional bundle report / budget gate. Exits non-zero if a budget
196
- * is exceeded. Isolated verify builds skip all of this.
274
+ * is exceeded. Isolated verify builds skip all of this except the bundle
275
+ * report / budget gate, which they run against their own output.
197
276
  */
198
277
  const publishBuildArtifacts = async (
199
278
  project: BlumeProject,
@@ -214,13 +293,8 @@ const publishBuildArtifacts = async (
214
293
  warn: (message) => logger.warn(message),
215
294
  });
216
295
 
217
- if (project.config.ai.llmsTxt) {
218
- const { index, full } = await buildLlmsFiles(project);
219
- await Promise.all([
220
- writeFile(join(distDir, "llms.txt"), index, "utf-8"),
221
- writeFile(join(distDir, "llms-full.txt"), full, "utf-8"),
222
- ]);
223
- logger.success("Generated llms.txt and llms-full.txt");
296
+ if (project.config.ai.llmsTxt.enabled) {
297
+ await publishLlmsFiles(project, distDir);
224
298
  }
225
299
 
226
300
  // A user's own public/ file (copied into dist by Astro) always wins.
@@ -253,6 +327,11 @@ const publishBuildArtifacts = async (
253
327
 
254
328
  const { config } = project;
255
329
  const features = serverFeatures(config);
330
+ // `buildSitemap` returns null both when the sitemap is disabled and when no
331
+ // `site` is configured — only the latter deserves the remediation hint.
332
+ const sitemapNote = config.seo.sitemap
333
+ ? "no (set deployment.site)"
334
+ : "no (seo.sitemap is false)";
256
335
  logger.box(
257
336
  [
258
337
  `Output ${config.deployment.output}`,
@@ -260,21 +339,15 @@ const publishBuildArtifacts = async (
260
339
  `Site ${config.deployment.site ?? "not set"}`,
261
340
  `Search ${config.search.provider}`,
262
341
  `Redirects ${config.redirects.length}`,
263
- `Sitemap ${sitemap ? "yes" : "no (set deployment.site)"}`,
342
+ `Sitemap ${sitemap ? "yes" : sitemapNote}`,
264
343
  `Robots ${robots ? "yes" : "no"}`,
265
344
  `Agent JSON ${agentReadability ? "yes" : "no"}`,
266
- `LLM files ${config.ai.llmsTxt ? "yes" : "no"}`,
345
+ `LLM files ${config.ai.llmsTxt.enabled ? "yes" : "no"}`,
267
346
  `Server features ${features.length > 0 ? features.join(", ") : "none"}`,
268
347
  ].join("\n")
269
348
  );
270
349
 
271
- if (args.analyze) {
272
- await reportBundleSizes(distDir);
273
- }
274
-
275
- if ((await enforceBudget(distDir, args)) === "fail") {
276
- process.exit(1);
277
- }
350
+ await runClientAssetChecks(distDir, args);
278
351
 
279
352
  logger.success(`Built to ${distDir}`);
280
353
  };
@@ -330,7 +403,7 @@ export const buildCommand = defineCommand({
330
403
  const runtimeDir = args.isolated
331
404
  ? ".blume-verify"
332
405
  : process.env.BLUME_RUNTIME_DIR;
333
- refuseIfDevRunning(root, "building", runtimeDir);
406
+ refuseIfDevRunning(root, "building", { isolatedHint: true, runtimeDir });
334
407
  if (args.isolated) {
335
408
  await ensureGitignore(root, [".blume-verify/"]);
336
409
  }
@@ -374,8 +447,15 @@ export const buildCommand = defineCommand({
374
447
  // An isolated build is a throwaway verify: it only needs to confirm the site
375
448
  // compiles and renders. Skip the network post-steps (search sync) and
376
449
  // deploy artifacts (index/llms/sitemap/robots/redirects) that only matter
377
- // for a real publish and would push to hosted providers.
450
+ // for a real publish and would push to hosted providers. The bundle report
451
+ // and budget gate still run, though — `blume build --isolated --budget-js
452
+ // 100` exiting 0 without measuring anything would be a silent false pass
453
+ // in CI.
378
454
  if (runtimeDir) {
455
+ await runClientAssetChecks(
456
+ isolatedStaticDir(project.config, project.context),
457
+ args
458
+ );
379
459
  logger.success(
380
460
  `Isolated build OK — output at ${distDir} (not published).`
381
461
  );
@@ -39,7 +39,7 @@ export const checkCommand = defineCommand({
39
39
  const runtimeDir = args.isolated
40
40
  ? ".blume-verify"
41
41
  : process.env.BLUME_RUNTIME_DIR;
42
- refuseIfDevRunning(root, "checking", runtimeDir);
42
+ refuseIfDevRunning(root, "checking", { isolatedHint: true, runtimeDir });
43
43
  if (args.isolated) {
44
44
  await ensureGitignore(root, [".blume-verify/"]);
45
45
  }
@@ -15,9 +15,21 @@ import {
15
15
  DevLockHeldError,
16
16
  updateDevLockPort,
17
17
  } from "../dev-lock.ts";
18
- import { logger } from "../log.ts";
18
+ import { logger, reportDiagnostics } from "../log.ts";
19
19
  import { prepareProject } from "../prepare.ts";
20
20
 
21
+ /**
22
+ * Resolve a `--host` flag value into what Astro/Vite's `server.host` expects.
23
+ * citty (0.1) has no mixed string/boolean arg type, so `host` is declared as a
24
+ * string and a bare `--host` parses as `""` — 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
+
21
33
  /**
22
34
  * A fingerprint of the route set: the sorted `path entryId` pairs. It changes
23
35
  * when a page is added, removed, or renamed (a folder rename shifts many at
@@ -100,13 +112,13 @@ export const devCommand = defineCommand({
100
112
 
101
113
  // A factory so `runRegenerate` can recreate the server on a structural
102
114
  // (route-set) change: only a cold container re-globs Astro's content store,
103
- // which its in-place config restart doesn't. `open` is honoured on first
115
+ // which its in-place config restart doesn't. `open` is honored on first
104
116
  // boot only — a restart must not reopen the browser.
105
117
  const createServer = (listenPort: number | undefined, open: boolean) =>
106
118
  dev({
107
119
  logLevel: args.debug ? "debug" : "info",
108
120
  root: project.context.outDir,
109
- server: { host: args.host ?? false, open, port: listenPort },
121
+ server: { host: normalizeHost(args.host), open, port: listenPort },
110
122
  });
111
123
 
112
124
  let server = await createServer(explicitPort, args.open ?? false);
@@ -148,7 +160,6 @@ export const devCommand = defineCommand({
148
160
  });
149
161
  const nextSignature = routeSignature(next.manifest.routes);
150
162
  const structural = nextSignature !== lastSignature;
151
- lastSignature = nextSignature;
152
163
  if (structural) {
153
164
  await server.stop();
154
165
  await generateRuntime(next);
@@ -156,7 +167,17 @@ export const devCommand = defineCommand({
156
167
  } else {
157
168
  await generateRuntime(next);
158
169
  }
159
- // Surface any content/config errors in the browser overlay too.
170
+ // Commit the signature only after the (re)generation succeeded. If the
171
+ // restart above throws mid-sequence, the signature stays stale so the
172
+ // next watch event retries the structural path — committing early would
173
+ // route it to the non-structural branch with the server still down.
174
+ lastSignature = nextSignature;
175
+ // Surface any content/config errors in the terminal AND the browser
176
+ // overlay. The terminal report must not be skipped: on a published
177
+ // install the CLI bundle holds its own copy of the integration module,
178
+ // separate from the Vite module graph that registers the overlay, so
179
+ // the overlay call below can be a no-op there.
180
+ reportDiagnostics(next.diagnostics, root);
160
181
  showBlumeErrorOverlay(next.diagnostics);
161
182
  } catch (error) {
162
183
  logger.error(`Regeneration failed: ${(error as Error).message}`);
@@ -1,28 +1,32 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
-
3
1
  import { defineCommand } from "citty";
4
- import { join, relative } from "pathe";
2
+ import { relative } from "pathe";
5
3
 
4
+ import { loadConfig } from "../../core/config.ts";
6
5
  import { eject } from "../../registry/eject.ts";
7
6
  import { refuseIfDevRunning } from "../dev-lock.ts";
7
+ import {
8
+ droppedArtifactNotices,
9
+ updatePackageScripts,
10
+ } from "../eject-scripts.ts";
11
+ import { commandsFor, detectPackageManager } from "../init/scaffold.ts";
8
12
  import { logger } from "../log.ts";
9
13
 
10
- const updatePackageScripts = async (root: string): Promise<void> => {
11
- const pkgPath = join(root, "package.json");
12
- let pkg: Record<string, unknown>;
13
- try {
14
- pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
15
- } catch {
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) {
16
22
  return;
17
23
  }
18
- const scripts = (pkg.scripts ?? {}) as Record<string, string>;
19
- pkg.scripts = {
20
- ...scripts,
21
- build: "astro build",
22
- dev: "astro dev",
23
- preview: "astro preview",
24
- };
25
- await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
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
+ );
26
30
  };
27
31
 
28
32
  export const ejectCommand = defineCommand({
@@ -37,23 +41,47 @@ export const ejectCommand = defineCommand({
37
41
  const root = process.cwd();
38
42
  refuseIfDevRunning(root, "ejecting");
39
43
 
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
+
40
56
  if (!args.yes) {
41
57
  logger.warn(
42
58
  "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."
43
59
  );
60
+ reportDroppedArtifacts(notices);
44
61
  logger.info("Re-run with --yes to proceed.");
45
62
  return;
46
63
  }
47
64
 
48
- const files = await eject(root);
65
+ const { files, warnings } = await eject(root);
49
66
  await updatePackageScripts(root);
50
67
 
68
+ // The same surface as the generated-runtime path (prepare.ts): one warn
69
+ // per generation warning, e.g. a Scalar reference spec that wasn't found.
70
+ for (const warning of warnings) {
71
+ logger.warn(warning);
72
+ }
73
+
51
74
  logger.success(`Ejected ${files.length} file(s):`);
52
75
  for (const file of files) {
53
76
  process.stdout.write(` ${relative(root, file)}\n`);
54
77
  }
78
+ reportDroppedArtifacts(notices);
79
+ // Print run commands matching the user's package manager, detected the
80
+ // same way as `blume init`'s next-steps hint.
81
+ const pm = detectPackageManager(process.env.npm_config_user_agent);
82
+ const { build, dev } = commandsFor(pm);
55
83
  logger.box(
56
- "Your project is now a standalone Astro app.\n\n bun run dev\n bun run build\n\nThe blume package remains importable."
84
+ `Your project is now a standalone Astro app.\n\n ${dev}\n ${build}\n\nThe blume package remains importable.`
57
85
  );
58
86
  },
59
87
  });