blume 1.0.4 → 1.1.1

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 (117) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/cli/index.js +13404 -10228
  3. package/dist/cli/index.js.map +94 -63
  4. package/dist/types/ai/component-markdown.d.ts +12 -1
  5. package/dist/types/core/config-input.d.ts +73 -4
  6. package/dist/types/core/data.d.ts +9 -0
  7. package/dist/types/core/deployment-env.d.ts +6 -0
  8. package/dist/types/core/diagnostics.d.ts +23 -0
  9. package/dist/types/core/i18n-ui.d.ts +8 -8
  10. package/dist/types/core/schema.d.ts +144 -22
  11. package/dist/types/core/sources/types.d.ts +3 -1
  12. package/dist/types/core/standard-schema.d.ts +41 -0
  13. package/dist/types/core/types.d.ts +20 -0
  14. package/dist/types/og/card.d.ts +63 -0
  15. package/dist/types/og/dimensions.d.ts +12 -0
  16. package/docs/01-quickstart.mdx +1 -1
  17. package/docs/02-deployment.mdx +9 -1
  18. package/docs/advanced/api-reference.mdx +11 -0
  19. package/docs/advanced/changelog.mdx +2 -2
  20. package/docs/advanced/skills.mdx +1 -1
  21. package/docs/configuration/ai.mdx +3 -3
  22. package/docs/configuration/customization.mdx +1 -1
  23. package/docs/configuration/export.mdx +1 -1
  24. package/docs/configuration/index.mdx +21 -1
  25. package/docs/configuration/search.mdx +28 -1
  26. package/docs/configuration/seo.mdx +37 -2
  27. package/docs/configuration/theming.mdx +1 -1
  28. package/docs/content/components.mdx +14 -0
  29. package/docs/content/index.mdx +1 -1
  30. package/docs/content/meta.mdx +1 -1
  31. package/docs/content/navigation.mdx +5 -1
  32. package/docs/content/sources.mdx +1 -1
  33. package/docs/reference/cli.mdx +80 -2
  34. package/docs/reference/frontmatter.mdx +31 -1
  35. package/package.json +4 -3
  36. package/skills/blume-migrate/SKILL.md +1 -1
  37. package/skills/blume-migrate/references/mintlify.md +3 -2
  38. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +16 -4
  39. package/src/ai/component-markdown.ts +39 -11
  40. package/src/ai/llms.ts +19 -2
  41. package/src/ai/markdown.ts +5 -1
  42. package/src/astro/adapter-root.ts +70 -0
  43. package/src/astro/generate.ts +124 -50
  44. package/src/astro/index.ts +1 -0
  45. package/src/astro/pages.ts +39 -8
  46. package/src/astro/templates.ts +93 -28
  47. package/src/audit/agent.ts +114 -0
  48. package/src/audit/catalog.ts +826 -0
  49. package/src/audit/checks/assets.ts +177 -0
  50. package/src/audit/checks/content.ts +231 -0
  51. package/src/audit/checks/duplicates.ts +131 -0
  52. package/src/audit/checks/i18n.ts +246 -0
  53. package/src/audit/checks/indexability.ts +213 -0
  54. package/src/audit/checks/links.ts +223 -0
  55. package/src/audit/checks/llms.ts +138 -0
  56. package/src/audit/checks/network.ts +272 -0
  57. package/src/audit/checks/og-image.ts +113 -0
  58. package/src/audit/checks/redirects.ts +87 -0
  59. package/src/audit/checks/robots.ts +114 -0
  60. package/src/audit/checks/sitemap.ts +229 -0
  61. package/src/audit/checks/social.ts +238 -0
  62. package/src/audit/crawl.ts +259 -0
  63. package/src/audit/graph.ts +74 -0
  64. package/src/audit/html.ts +54 -0
  65. package/src/audit/image-size.ts +63 -0
  66. package/src/audit/locate.ts +33 -0
  67. package/src/audit/redirects.ts +74 -0
  68. package/src/audit/report.ts +278 -0
  69. package/src/audit/run.ts +198 -0
  70. package/src/audit/snapshot.ts +189 -0
  71. package/src/audit/types.ts +214 -0
  72. package/src/audit/url.ts +103 -0
  73. package/src/cli/commands/audit.ts +205 -0
  74. package/src/cli/commands/build.ts +64 -13
  75. package/src/cli/index.ts +2 -0
  76. package/src/cli/prepare.ts +10 -2
  77. package/src/components/content/Tabs.astro +98 -15
  78. package/src/components/layout/Breadcrumbs.astro +1 -1
  79. package/src/components/layout/Header.astro +1 -0
  80. package/src/components/layout/PageFeedback.astro +1 -1
  81. package/src/components/layout/PageLayout.astro +5 -1
  82. package/src/components/layout/Pagination.astro +1 -1
  83. package/src/components/layout/RootLayout.astro +5 -3
  84. package/src/components/layout/Search.astro +35 -6
  85. package/src/components/layout/TableOfContents.astro +1 -1
  86. package/src/components/openapi/Authorization.astro +80 -0
  87. package/src/components/openapi/Operation.astro +19 -1
  88. package/src/components/openapi/ParametersTable.astro +1 -1
  89. package/src/components/openapi/security.ts +201 -0
  90. package/src/components/openapi/snippets.ts +42 -13
  91. package/src/core/config-input.ts +78 -4
  92. package/src/core/data.ts +9 -1
  93. package/src/core/deployment-env.ts +9 -0
  94. package/src/core/diagnostics.ts +61 -12
  95. package/src/core/graph.ts +23 -4
  96. package/src/core/links.ts +2 -91
  97. package/src/core/nav-diagnostics.ts +48 -4
  98. package/src/core/navigation.ts +169 -14
  99. package/src/core/probe.ts +136 -0
  100. package/src/core/project-graph.ts +54 -20
  101. package/src/core/schema.ts +93 -3
  102. package/src/core/sources/github-releases.ts +65 -2
  103. package/src/core/sources/normalize.ts +198 -25
  104. package/src/core/sources/types.ts +3 -1
  105. package/src/core/standard-schema.ts +54 -0
  106. package/src/core/types.ts +20 -0
  107. package/src/deploy/adapter-output.ts +27 -15
  108. package/src/deploy/headers.ts +66 -0
  109. package/src/deploy/redirects.ts +49 -9
  110. package/src/markdown/index.ts +1 -0
  111. package/src/markdown/twoslash.ts +60 -0
  112. package/src/og/card.ts +98 -33
  113. package/src/og/index.ts +1 -1
  114. package/src/registry/eject.ts +3 -1
  115. package/src/search/popular.ts +33 -0
  116. package/src/theme/entry.ts +6 -1
  117. /package/docs/{03-faq.mdx → 07-faq.mdx} +0 -0
@@ -10,7 +10,7 @@ import type { ResolvedConfig } from "../core/schema.ts";
10
10
  import { BLUME_IGNORE_DIRS } from "../core/sources/watch.ts";
11
11
  import { trimChar } from "../core/trim.ts";
12
12
  import type { ProjectContext } from "../core/types.ts";
13
- import { applyBaseToRedirects } from "../deploy/redirects.ts";
13
+ import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
14
14
  import { hasScalarReferences } from "../openapi/references.ts";
15
15
  import { searchProviderMeta } from "../search/providers.ts";
16
16
  import { buildFontEntries } from "../theme/fonts.ts";
@@ -166,11 +166,16 @@ export const runtimeDependencies = (options: {
166
166
  * static-prerender Vite environments.
167
167
  *
168
168
  * Two reasons a dep lands here:
169
- * - `@takumi-rs/core` (OG image rendering) is a native NAPI addon that loads a
170
- * platform-specific `.node` binding via `createRequire(import.meta.url)`.
171
- * Bundling it relocates `import.meta.url` and breaks the binding lookup
172
- * ("Cannot find native binding") on other platforms (e.g. the Linux CI
173
- * runner), so it must resolve from `node_modules` at runtime instead.
169
+ * - `takumi-js` (OG image rendering) loads `@takumi-rs/core`, a native NAPI
170
+ * addon that finds its platform-specific `.node` binding via
171
+ * `createRequire(import.meta.url)`. Bundling it relocates `import.meta.url`
172
+ * and breaks the binding lookup ("Cannot find native binding") on other
173
+ * platforms (e.g. the Linux CI runner), so it must resolve from
174
+ * `node_modules` at runtime instead. The prerender env matches these by
175
+ * exact specifier, so every entry point Blume imports has to be listed:
176
+ * the bare `takumi-js` (render) plus `takumi-js/helpers` (the `googleFonts`
177
+ * OG-font loader). The `@takumi-rs/*` packages are listed too so the native
178
+ * backend is never pulled into a chunk down any transitive path.
174
179
  * - The rest are pure-JS packages kept external so an isolated linker (Bun's
175
180
  * `isolated` mode, pnpm) doesn't bundle their symlinked store copies. When
176
181
  * Vite bundles such a package but leaves its own `node_modules` child
@@ -190,10 +195,13 @@ const RENDER_EXTERNAL_DEPS = [
190
195
  "@shikijs/transformers",
191
196
  "@takumi-rs/core",
192
197
  "@takumi-rs/helpers",
198
+ "@takumi-rs/wasm",
193
199
  "github-slugger",
194
200
  "katex",
195
201
  "shiki",
196
202
  "simple-icons",
203
+ "takumi-js",
204
+ "takumi-js/helpers",
197
205
  "zod",
198
206
  ];
199
207
 
@@ -212,6 +220,21 @@ const renderUserAliases = (
212
220
  const astroOutDir = (context: ProjectContext): string =>
213
221
  context.distDir ?? `${context.root}/dist`;
214
222
 
223
+ /**
224
+ * The root a deploy adapter is shown, in place of the `.blume` runtime Astro
225
+ * actually roots at. Adapters assume `outDir` is `<root>/dist` and resolve their
226
+ * own output (and Vercel's dependency trace) against `root`, so the root implied
227
+ * by Blume's `outDir` is the one that keeps that assumption true. See
228
+ * {@link withAdapterRoot}.
229
+ *
230
+ * For a normal build that is the project root (`<project>/dist` -> `<project>`).
231
+ * For a relocated runtime (`blume build --isolated`) it is the runtime dir
232
+ * itself (`<runtime>/dist` -> `<runtime>`), keeping a verify build's adapter
233
+ * output self-contained instead of overwriting the real `.vercel/output`.
234
+ */
235
+ const adapterRoot = (context: ProjectContext): string =>
236
+ dirname(astroOutDir(context));
237
+
215
238
  /**
216
239
  * Excludes Vite's pre-bundled dep cache from @vitejs/plugin-react. Astro's
217
240
  * react() replaces the plugin's default `/node_modules/` exclude with just
@@ -296,8 +319,17 @@ export const astroConfigTemplate = (options: {
296
319
  }
297
320
  return ADAPTER_OPTIONS[deployment.adapter] ?? "";
298
321
  })();
322
+ // Vercel resolves its Build Output tree and its `@vercel/nft` dependency
323
+ // trace against the Astro root, which for Blume is the hidden `.blume`
324
+ // runtime — leaving the traced function without its chunks or node_modules.
325
+ // The other adapters emit into `outDir` (cloudflare, node) or are surfaced
326
+ // afterwards (netlify), so none of them read `root` this way.
327
+ const adapterExpr =
328
+ deployment.adapter === "vercel"
329
+ ? `withAdapterRoot(adapter(${adapterArgs}), ${JSON.stringify(adapterRoot(context))})`
330
+ : `adapter(${adapterArgs})`;
299
331
  const adapterOption =
300
- server && deployment.adapter ? `\n adapter: adapter(${adapterArgs}),` : "";
332
+ server && deployment.adapter ? `\n adapter: ${adapterExpr},` : "";
301
333
 
302
334
  const siteOption = deployment.site
303
335
  ? `\n site: ${JSON.stringify(deployment.site)},`
@@ -320,10 +352,12 @@ export const astroConfigTemplate = (options: {
320
352
  : "";
321
353
 
322
354
  // Base the redirect paths the same way routes are based, so a redirect lands
323
- // under `basePath` too. Astro layers its own `base` (deployment.base) on top.
324
- const basedRedirects = applyBaseToRedirects(
355
+ // under `basePath` too. Astro layers its own `base` (deployment.base) onto
356
+ // `from` when matching, but never onto `to` — see applyBaseToAstroRedirects.
357
+ const basedRedirects = applyBaseToAstroRedirects(
325
358
  config.redirects,
326
- config.basePath
359
+ config.basePath,
360
+ deployment.base ?? ""
327
361
  );
328
362
  const redirectsOption =
329
363
  basedRedirects.length > 0
@@ -365,14 +399,21 @@ export const astroConfigTemplate = (options: {
365
399
  const svelteImport = needsSvelte
366
400
  ? `import svelte from "@astrojs/svelte";\n`
367
401
  : "";
368
- const blumeImport = `import { blumeIntegration, prerenderDepsPlugin, serverAppResolvePlugin } from "blume/astro";\n`;
402
+ const blumeImports = [
403
+ "blumeIntegration",
404
+ "prerenderDepsPlugin",
405
+ "serverAppResolvePlugin",
406
+ ...(adapterOption.includes("withAdapterRoot") ? ["withAdapterRoot"] : []),
407
+ ];
408
+ const blumeImport = `import { ${blumeImports.join(", ")} } from "blume/astro";\n`;
369
409
 
370
410
  // Twoslash runs first, before the always-on transformers, but only on fences
371
411
  // with the `twoslash` meta (explicitTrigger) — so it's opt-in per block with
372
412
  // no config flag; the TypeScript compiler only spins up when a block uses it.
373
- const twoslashImport = `import { transformerTwoslash } from "@shikijs/twoslash";\n`;
374
- const twoslashTransformer =
375
- "transformerTwoslash({ explicitTrigger: true }), ";
413
+ // Blume's preconfigured transformer compiles with the package's own pinned
414
+ // classic TypeScript, so the user's project can be on any version (see
415
+ // markdown/twoslash.ts).
416
+ const twoslashTransformer = "blumeTwoslashTransformer(), ";
376
417
 
377
418
  // Content links are rewritten to their real served URL: the `deployment.base`
378
419
  // subdirectory (Astro doesn't rewrite `<a href>`) layered over the site-wide
@@ -409,8 +450,8 @@ export const astroConfigTemplate = (options: {
409
450
  ${defineConfigImport}
410
451
  import mdx from "@astrojs/mdx";
411
452
  import tailwindcss from "@tailwindcss/vite";
412
- import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers } from "blume/markdown";
413
- ${twoslashImport}${reactImport}${vueImport}${svelteImport}${blumeImport}${adapterImport}
453
+ import { blumeMarkdownProcessor, blumeMdxProcessor, blumeShikiTransformers, blumeTwoslashTransformer } from "blume/markdown";
454
+ ${reactImport}${vueImport}${svelteImport}${blumeImport}${adapterImport}
414
455
  export default defineConfig({
415
456
  root: ${JSON.stringify(context.outDir)},
416
457
  srcDir: ${JSON.stringify(`${context.outDir}/src`)},
@@ -1037,7 +1078,9 @@ import data from "blume:data";
1037
1078
  export const prerender = true;
1038
1079
 
1039
1080
  // Custom (non-content) pages opted into a generated card, baked in at build.
1040
- const customRoutes = ${JSON.stringify(customRoutes)};
1081
+ // The annotation keeps the empty-array case from being an implicit any[]
1082
+ // (ts(7034)) under a strict tsconfig.
1083
+ const customRoutes: { slug: string; title: string }[] = ${JSON.stringify(customRoutes)};
1041
1084
 
1042
1085
  export function getStaticPaths() {
1043
1086
  const seen = new Set<string>();
@@ -1080,6 +1123,7 @@ export async function GET({ props }: { props: { title: string } }) {
1080
1123
  accent: data.config.og.palette?.accent ?? data.config.theme.accent.light,
1081
1124
  brand: data.config.title,
1082
1125
  description: data.config.description,
1126
+ fonts: data.config.og.fonts,
1083
1127
  logo: data.config.og.logo,
1084
1128
  palette: data.config.og.palette,
1085
1129
  repo: repoSlug,
@@ -1475,6 +1519,18 @@ const toTime = (value: string | null | undefined) => {
1475
1519
  return Number.isNaN(date.getTime()) ? 0 : date.getTime();
1476
1520
  };
1477
1521
 
1522
+ // The changelog is an unlocalized route, so its chrome renders in the default
1523
+ // locale's dictionary and direction (\`data.ui\` is the default locale's resolved
1524
+ // dictionary), mirroring the catch-all's locale wiring.
1525
+ const i18n = data.config.i18n;
1526
+ const localeMeta = i18n
1527
+ ? i18n.locales.find((l) => l.code === i18n.defaultLocale)
1528
+ : null;
1529
+ const dir = localeMeta?.dir ?? "ltr";
1530
+ const htmlLang = i18n ? i18n.defaultLocale : "en";
1531
+
1532
+ // Formatted in the same locale as the chrome, and in UTC, to match the
1533
+ // per-page "last updated" stamp.
1478
1534
  const formatDate = (value: string | null | undefined) => {
1479
1535
  if (!value) {
1480
1536
  return;
@@ -1482,7 +1538,7 @@ const formatDate = (value: string | null | undefined) => {
1482
1538
  const date = new Date(value);
1483
1539
  return Number.isNaN(date.getTime())
1484
1540
  ? undefined
1485
- : new Intl.DateTimeFormat("en", {
1541
+ : new Intl.DateTimeFormat(htmlLang, {
1486
1542
  dateStyle: "long",
1487
1543
  timeZone: "UTC",
1488
1544
  }).format(date);
@@ -1579,16 +1635,6 @@ const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
1579
1635
  const basedRoute = withBase("/changelog");
1580
1636
  const canonical = base ? base + basedRoute : null;
1581
1637
 
1582
- // The changelog is an unlocalized route, so its chrome renders in the default
1583
- // locale's dictionary and direction (\`data.ui\` is the default locale's resolved
1584
- // dictionary), mirroring the catch-all's locale wiring.
1585
- const i18n = data.config.i18n;
1586
- const localeMeta = i18n
1587
- ? i18n.locales.find((l) => l.code === i18n.defaultLocale)
1588
- : null;
1589
- const dir = localeMeta?.dir ?? "ltr";
1590
- const htmlLang = i18n ? i18n.defaultLocale : "en";
1591
-
1592
1638
  // The page chrome (h1, title, description) comes from the same translatable
1593
1639
  // \`changelog\` group as the reveal button; optional chaining tolerates a
1594
1640
  // not-yet-regenerated data snapshot from before these keys existed.
@@ -1760,6 +1806,23 @@ const islandDirective = (spec: IslandSpec): string =>
1760
1806
  ? `client:only="${spec.framework}"`
1761
1807
  : `client:${spec.client}`;
1762
1808
 
1809
+ /**
1810
+ * Frontmatter `Props` alias mirroring the wrapped component's own props, so
1811
+ * `{...Astro.props}` satisfies required props under `astro check` (the spread
1812
+ * of an untyped `Astro.props` contributes nothing to the JSX props type).
1813
+ * `infer P extends object` rather than `Record<string, unknown>` because
1814
+ * interfaces have no implicit index signature and would miss the narrower
1815
+ * constraint. Non-function component types (Vue/Svelte ambient modules) fall
1816
+ * back to an open record, keeping the untyped permissiveness they had.
1817
+ */
1818
+ const wrapperPropsType = (name: string): string =>
1819
+ `type Props = typeof ${name} extends (
1820
+ props: infer P extends object,
1821
+ ...rest: never[]
1822
+ ) => unknown
1823
+ ? P
1824
+ : Record<string, unknown>;`;
1825
+
1763
1826
  /**
1764
1827
  * Generate `.blume/src/generated/islands/<Name>.astro` — a wrapper that renders
1765
1828
  * a convention island with its hydration directive applied. Astro client
@@ -1770,6 +1833,7 @@ export const islandWrapperTemplate = (spec: IslandSpec): string =>
1770
1833
  `---
1771
1834
  // Generated by Blume. Do not edit.
1772
1835
  import Island from ${JSON.stringify(spec.file)};
1836
+ ${wrapperPropsType("Island")}
1773
1837
  ---
1774
1838
  <Island ${islandDirective(spec)} {...Astro.props}><slot /></Island>
1775
1839
  `;
@@ -1832,6 +1896,7 @@ export const exampleWrapperTemplate = (spec: ExampleSpec): string =>
1832
1896
  `---
1833
1897
  // Generated by Blume. Do not edit.
1834
1898
  import Example from ${JSON.stringify(spec.file)};
1899
+ ${wrapperPropsType("Example")}
1835
1900
  ---
1836
1901
  <Example ${exampleDirective(spec)}{...Astro.props}><slot /></Example>
1837
1902
  `;
@@ -0,0 +1,114 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtemp, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+
5
+ import { join } from "pathe";
6
+
7
+ import { reportJson } from "./report.ts";
8
+ import type { AuditResult } from "./run.ts";
9
+
10
+ /** A coding agent CLI the audit can hand its findings to (`--claude`, `--codex`). */
11
+ export interface AgentCli {
12
+ /** The executable to look up on PATH. */
13
+ bin: string;
14
+ /** How to install it, shown when the executable is missing. */
15
+ install: string;
16
+ /** Display name for messages. */
17
+ name: string;
18
+ }
19
+
20
+ export type AgentKind = "claude" | "codex";
21
+
22
+ export const AGENTS: Record<AgentKind, AgentCli> = {
23
+ claude: {
24
+ bin: "claude",
25
+ install: "npm install -g @anthropic-ai/claude-code",
26
+ name: "Claude Code",
27
+ },
28
+ codex: {
29
+ bin: "codex",
30
+ install: "npm install -g @openai/codex",
31
+ name: "Codex",
32
+ },
33
+ };
34
+
35
+ /**
36
+ * Write the full JSON report where the agent can read it. A file rather than
37
+ * inline prompt text: a large site's report can exceed the platform's argv
38
+ * limit, and the JSON already carries every finding untruncated — the terminal
39
+ * report previews three pages per check, the file never does.
40
+ */
41
+ export const writeAgentReport = async (
42
+ result: AuditResult,
43
+ root: string
44
+ ): Promise<string> => {
45
+ const dir = await mkdtemp(join(tmpdir(), "blume-audit-"));
46
+ const path = join(dir, "report.json");
47
+ await writeFile(path, reportJson(result, root));
48
+ return path;
49
+ };
50
+
51
+ /** The handoff prompt: where the report is, how to read it, and the ground rules. */
52
+ export const fixPrompt = (reportPath: string): string =>
53
+ `Fix the issues found by \`blume audit\` in this project.
54
+
55
+ The full audit report is at ${reportPath}. It is JSON: each entry in \`diagnostics\` is one finding, with the check \`code\`, a \`message\` explaining what is wrong, the affected page \`url\`, the source \`file\` to edit (relative to the current directory, with a \`line\` when the finding points at a specific front matter key), and a \`suggestion\` describing the fix.
56
+
57
+ Work through every finding:
58
+ 1. Read the report and group the findings by \`file\`.
59
+ 2. Apply each finding's \`suggestion\` by editing the named source file — most fixes are front matter edits at the cited line.
60
+ 3. Never fix a finding by deleting a page, removing content, or hiding it from the audit; if a finding genuinely needs a human decision, leave it and say so in your summary.
61
+
62
+ When you are done, run \`blume build\` and then \`blume audit\` to verify, and repeat until the audit reports no issues.`;
63
+
64
+ const spawnAgent = (
65
+ command: string,
66
+ args: string[],
67
+ shell: boolean
68
+ ): Promise<number> =>
69
+ // oxlint-disable-next-line promise/avoid-new -- adapt spawn's event callbacks
70
+ new Promise((resolve, reject) => {
71
+ const child = spawn(command, args, { shell, stdio: "inherit" });
72
+ child.once("error", reject);
73
+ child.once("close", (code) => resolve(code ?? 1));
74
+ });
75
+
76
+ /**
77
+ * cmd.exe reports a missing executable through this exit code instead of a
78
+ * spawn error, so a shell launch can't rely on the `error` event for the
79
+ * "not installed" diagnosis.
80
+ */
81
+ export const WINDOWS_COMMAND_NOT_FOUND = 9009;
82
+
83
+ /**
84
+ * Run the agent CLI interactively with the handoff prompt, inheriting the
85
+ * terminal so the user watches and steers the fixes rather than granting a
86
+ * headless process blanket write access. Resolves with the agent's exit code;
87
+ * rejects when the executable isn't on PATH.
88
+ *
89
+ * On Windows, npm installs agent CLIs as `.cmd` shims, which Node refuses to
90
+ * spawn without a shell — and cmd.exe cannot carry the multi-line prompt as an
91
+ * argument (a newline ends the command). So there the prompt is written to a
92
+ * file next to the report and handed over via a one-line pointer that survives
93
+ * cmd.exe quoting; a missing executable surfaces as
94
+ * {@link WINDOWS_COMMAND_NOT_FOUND} rather than a rejection.
95
+ */
96
+ export const launchAgent = async (
97
+ bin: string,
98
+ prompt: string,
99
+ platform: NodeJS.Platform = process.platform
100
+ ): Promise<number> => {
101
+ if (platform !== "win32") {
102
+ return await spawnAgent(bin, [prompt], false);
103
+ }
104
+ const dir = await mkdtemp(join(tmpdir(), "blume-audit-"));
105
+ const promptPath = join(dir, "prompt.md");
106
+ await writeFile(promptPath, prompt);
107
+ // Double quotes are the one grouping cmd.exe respects; neither the temp
108
+ // path nor the fixed pointer text can contain one.
109
+ return await spawnAgent(
110
+ `"${bin}" "Read ${promptPath} and follow its instructions exactly."`,
111
+ [],
112
+ true
113
+ );
114
+ };