blume 1.6.4 → 1.6.6

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 (178) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/bin/blume.mjs +3 -2
  3. package/dist/cli/chunk-0ewz4trd.js +679 -0
  4. package/dist/cli/chunk-0ewz4trd.js.map +15 -0
  5. package/dist/cli/chunk-27gtm2ym.js +69 -0
  6. package/dist/cli/chunk-27gtm2ym.js.map +11 -0
  7. package/dist/cli/chunk-2aj8ddew.js +72 -0
  8. package/dist/cli/chunk-2aj8ddew.js.map +10 -0
  9. package/dist/cli/chunk-3k0kzs6d.js +69 -0
  10. package/dist/cli/chunk-3k0kzs6d.js.map +11 -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-5hs6gb7n.js +32 -0
  18. package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
  19. package/dist/cli/chunk-5yvt556e.js +185 -0
  20. package/dist/cli/chunk-5yvt556e.js.map +11 -0
  21. package/dist/cli/chunk-62qsssnh.js +3808 -0
  22. package/dist/cli/chunk-62qsssnh.js.map +36 -0
  23. package/dist/cli/chunk-6kzzpsx8.js +26 -0
  24. package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
  25. package/dist/cli/chunk-8gnpdsn1.js +952 -0
  26. package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
  27. package/dist/cli/chunk-9sh49q0h.js +30 -0
  28. package/dist/cli/chunk-9sh49q0h.js.map +10 -0
  29. package/dist/cli/chunk-aerwpe14.js +2370 -0
  30. package/dist/cli/chunk-aerwpe14.js.map +15 -0
  31. package/dist/cli/chunk-ag1zyr5x.js +176 -0
  32. package/dist/cli/chunk-ag1zyr5x.js.map +10 -0
  33. package/dist/cli/chunk-bawgnt8x.js +277 -0
  34. package/dist/cli/chunk-bawgnt8x.js.map +11 -0
  35. package/dist/cli/chunk-bcy492zc.js +16 -0
  36. package/dist/cli/chunk-bcy492zc.js.map +10 -0
  37. package/dist/cli/chunk-btfr9yvw.js +41 -0
  38. package/dist/cli/chunk-btfr9yvw.js.map +10 -0
  39. package/dist/cli/chunk-cbjnx4s8.js +73 -0
  40. package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
  41. package/dist/cli/chunk-cnvm6k3e.js +96 -0
  42. package/dist/cli/chunk-cnvm6k3e.js.map +10 -0
  43. package/dist/cli/chunk-etsqspj6.js +5170 -0
  44. package/dist/cli/chunk-etsqspj6.js.map +47 -0
  45. package/dist/cli/chunk-ev67ycx0.js +15 -0
  46. package/dist/cli/chunk-ev67ycx0.js.map +10 -0
  47. package/dist/cli/chunk-ey89bjj1.js +209 -0
  48. package/dist/cli/chunk-ey89bjj1.js.map +11 -0
  49. package/dist/cli/chunk-f75cqye8.js +76 -0
  50. package/dist/cli/chunk-f75cqye8.js.map +10 -0
  51. package/dist/cli/chunk-j00ezcg5.js +259 -0
  52. package/dist/cli/chunk-j00ezcg5.js.map +11 -0
  53. package/dist/cli/chunk-jtb45atp.js +467 -0
  54. package/dist/cli/chunk-jtb45atp.js.map +14 -0
  55. package/dist/cli/chunk-m3p3wahd.js +117 -0
  56. package/dist/cli/chunk-m3p3wahd.js.map +10 -0
  57. package/dist/cli/chunk-n0y172hf.js +387 -0
  58. package/dist/cli/chunk-n0y172hf.js.map +12 -0
  59. package/dist/cli/chunk-n4qjabmt.js +1062 -0
  60. package/dist/cli/chunk-n4qjabmt.js.map +25 -0
  61. package/dist/cli/chunk-nyqzjdhj.js +111 -0
  62. package/dist/cli/chunk-nyqzjdhj.js.map +11 -0
  63. package/dist/cli/chunk-pxj10x8y.js +35 -0
  64. package/dist/cli/chunk-pxj10x8y.js.map +10 -0
  65. package/dist/cli/chunk-s4jn7f1q.js +54 -0
  66. package/dist/cli/chunk-s4jn7f1q.js.map +10 -0
  67. package/dist/cli/chunk-s4k1pnvf.js +81 -0
  68. package/dist/cli/chunk-s4k1pnvf.js.map +10 -0
  69. package/dist/cli/chunk-s5e5jt53.js +227 -0
  70. package/dist/cli/chunk-s5e5jt53.js.map +11 -0
  71. package/dist/cli/chunk-sbdqrjbb.js +81 -0
  72. package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
  73. package/dist/cli/chunk-tc89yh2r.js +136 -0
  74. package/dist/cli/chunk-tc89yh2r.js.map +10 -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-vv237fp3.js +1002 -0
  78. package/dist/cli/chunk-vv237fp3.js.map +13 -0
  79. package/dist/cli/chunk-vv3f8mb6.js +5314 -0
  80. package/dist/cli/chunk-vv3f8mb6.js.map +58 -0
  81. package/dist/cli/chunk-vxv4x1n8.js +17 -0
  82. package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
  83. package/dist/cli/chunk-wb067mv3.js +758 -0
  84. package/dist/cli/chunk-wb067mv3.js.map +13 -0
  85. package/dist/cli/chunk-wd27zjcz.js +60 -0
  86. package/dist/cli/chunk-wd27zjcz.js.map +10 -0
  87. package/dist/cli/chunk-wkq5tbtq.js +1141 -0
  88. package/dist/cli/chunk-wkq5tbtq.js.map +19 -0
  89. package/dist/cli/chunk-x1vrdjyk.js +1967 -0
  90. package/dist/cli/chunk-x1vrdjyk.js.map +34 -0
  91. package/dist/cli/chunk-x66c5yjn.js +23 -0
  92. package/dist/cli/chunk-x66c5yjn.js.map +10 -0
  93. package/dist/cli/index.js +55 -27587
  94. package/dist/cli/index.js.map +5 -243
  95. package/dist/types/ai/ask-context.d.ts +26 -0
  96. package/dist/types/core/code-fences.d.ts +11 -0
  97. package/dist/types/core/config-input.d.ts +10 -0
  98. package/dist/types/core/package-root.d.ts +1 -1
  99. package/dist/types/core/schema.d.ts +74 -1
  100. package/docs/02-deployment.mdx +1 -1
  101. package/docs/configuration/analytics.mdx +21 -2
  102. package/docs/configuration/ask-ai.mdx +1 -1
  103. package/docs/configuration/customization.mdx +2 -9
  104. package/docs/content/syntax.mdx +1 -1
  105. package/docs/reference/cli.mdx +1 -1
  106. package/package.json +16 -14
  107. package/src/ai/api/handlers.ts +4 -7
  108. package/src/ai/api/paths.ts +8 -0
  109. package/src/ai/api/spec.ts +2 -1
  110. package/src/ai/ask-context.ts +378 -22
  111. package/src/astro/generate.ts +25 -29
  112. package/src/astro/include-hmr.ts +10 -13
  113. package/src/astro/include-refresh.ts +0 -0
  114. package/src/astro/index.ts +6 -1
  115. package/src/astro/integration.ts +269 -53
  116. package/src/astro/module-types.ts +74 -0
  117. package/src/astro/templates.ts +85 -97
  118. package/src/audit/image-size.ts +10 -8
  119. package/src/cli/command-meta.ts +77 -0
  120. package/src/cli/commands/add.ts +2 -4
  121. package/src/cli/commands/audit.ts +2 -4
  122. package/src/cli/commands/build.ts +42 -346
  123. package/src/cli/commands/check.ts +2 -4
  124. package/src/cli/commands/dev.ts +31 -42
  125. package/src/cli/commands/doctor.ts +2 -4
  126. package/src/cli/commands/eject.ts +3 -41
  127. package/src/cli/commands/eval.ts +2 -5
  128. package/src/cli/commands/init.ts +2 -4
  129. package/src/cli/commands/mcp-stdio.ts +2 -5
  130. package/src/cli/commands/preview.ts +3 -5
  131. package/src/cli/commands/sync.ts +2 -4
  132. package/src/cli/commands/translate.ts +2 -5
  133. package/src/cli/commands/validate.ts +2 -4
  134. package/src/cli/commands/version.ts +2 -4
  135. package/src/cli/eject-scripts.ts +0 -45
  136. package/src/cli/host-args.ts +16 -0
  137. package/src/cli/index.ts +84 -35
  138. package/src/cli/lazy-command.ts +47 -0
  139. package/src/components/content/GithubInfo.astro +4 -1
  140. package/src/components/content/mermaid-element.ts +8 -0
  141. package/src/components/layout/Analytics.astro +20 -1
  142. package/src/components/layout/PageLayout.astro +14 -3
  143. package/src/components/layout/ReferenceLayout.astro +15 -4
  144. package/src/components/layout/RootLayout.astro +15 -4
  145. package/src/components/layout/analytics-client.ts +2 -1
  146. package/src/components/layout/page-locale.ts +29 -0
  147. package/src/components/openapi/AsyncApiOperation.astro +5 -3
  148. package/src/components/openapi/Authorization.astro +4 -6
  149. package/src/components/openapi/Bindings.astro +2 -2
  150. package/src/components/openapi/Description.astro +109 -0
  151. package/src/components/openapi/GraphqlFieldsTable.astro +5 -7
  152. package/src/components/openapi/GraphqlOperation.astro +4 -3
  153. package/src/components/openapi/GraphqlType.astro +4 -6
  154. package/src/components/openapi/ParametersTable.astro +5 -7
  155. package/src/components/openapi/RequestBody.astro +2 -4
  156. package/src/components/openapi/Responses.astro +4 -3
  157. package/src/components/openapi/SchemaProperty.astro +11 -6
  158. package/src/components/openapi/description.ts +91 -0
  159. package/src/core/api-name.ts +18 -0
  160. package/src/core/code-fences.ts +48 -0
  161. package/src/core/config-input.ts +10 -0
  162. package/src/core/content-assets.ts +3 -7
  163. package/src/core/includes.ts +3 -7
  164. package/src/core/package-root.ts +1 -1
  165. package/src/core/schema.ts +27 -0
  166. package/src/core/sources/normalize.ts +2 -37
  167. package/src/core/sources/obsidian.ts +3 -2
  168. package/src/core/svg-dimensions.ts +97 -0
  169. package/src/core/version-cut.ts +2 -2
  170. package/src/deploy/artifacts.ts +370 -0
  171. package/src/deploy/cloudflare-negotiation.ts +97 -32
  172. package/src/deploy/function-bundle.ts +66 -20
  173. package/src/deploy/sitemap.ts +6 -0
  174. package/src/deploy/vercel-negotiation.ts +8 -30
  175. package/src/og/card.ts +6 -12
  176. package/src/openapi/render-mdx.ts +9 -5
  177. package/src/registry/eject.ts +0 -2
  178. package/src/theme/entry.ts +9 -2
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ prepareProject
4
+ } from "./chunk-nyqzjdhj.js";
5
+ import"./chunk-9sh49q0h.js";
6
+ import"./chunk-btfr9yvw.js";
7
+ import {
8
+ readDevLock
9
+ } from "./chunk-tc89yh2r.js";
10
+ import"./chunk-62qsssnh.js";
11
+ import"./chunk-n4qjabmt.js";
12
+ import"./chunk-pxj10x8y.js";
13
+ import"./chunk-sbdqrjbb.js";
14
+ import"./chunk-5yvt556e.js";
15
+ import"./chunk-ev67ycx0.js";
16
+ import"./chunk-vt8fgygt.js";
17
+ import"./chunk-3r94j3tc.js";
18
+ import"./chunk-aerwpe14.js";
19
+ import"./chunk-3k0kzs6d.js";
20
+ import"./chunk-vxv4x1n8.js";
21
+ import"./chunk-wd27zjcz.js";
22
+ import"./chunk-etsqspj6.js";
23
+ import {
24
+ loadConfig,
25
+ resolveProjectContext,
26
+ resolveRuntimeDir
27
+ } from "./chunk-vv3f8mb6.js";
28
+ import"./chunk-27gtm2ym.js";
29
+ import"./chunk-4xyggvgf.js";
30
+ import"./chunk-6kzzpsx8.js";
31
+ import {
32
+ logger
33
+ } from "./chunk-ey89bjj1.js";
34
+ import {
35
+ commandMeta
36
+ } from "./chunk-2aj8ddew.js";
37
+
38
+ // src/cli/commands/sync.ts
39
+ import { rm } from "node:fs/promises";
40
+ import { defineCommand } from "citty";
41
+ import { join } from "pathe";
42
+ var syncCommand = defineCommand({
43
+ args: {
44
+ force: {
45
+ description: "Clear the source cache before refetching.",
46
+ type: "boolean"
47
+ },
48
+ preview: {
49
+ description: "Include drafts and unpublished CMS content.",
50
+ type: "boolean"
51
+ },
52
+ strict: { description: "Fail on diagnostics.", type: "boolean" }
53
+ },
54
+ meta: commandMeta.sync,
55
+ async run({ args }) {
56
+ const root = process.cwd();
57
+ if (args.force) {
58
+ const { config } = await loadConfig(root);
59
+ const context = resolveProjectContext(root, config);
60
+ await rm(join(context.outDir, "cache"), { force: true, recursive: true });
61
+ logger.info("Cleared source cache.");
62
+ }
63
+ const lock = readDevLock(resolveRuntimeDir(root));
64
+ const devServerUrl = lock?.port ? `http://localhost:${lock.port}` : undefined;
65
+ await prepareProject({
66
+ devServerUrl,
67
+ mode: "dev",
68
+ preview: args.preview,
69
+ refresh: true,
70
+ root,
71
+ strict: args.strict
72
+ });
73
+ logger.success("Synced content sources.");
74
+ }
75
+ });
76
+ export {
77
+ syncCommand
78
+ };
79
+
80
+ //# debugId=CD2123C1322DDD3C64756E2164756E21
81
+ //# sourceMappingURL=chunk-s4k1pnvf.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/cli/commands/sync.ts"],
4
+ "sourcesContent": [
5
+ "import { rm } from \"node:fs/promises\";\n\nimport { defineCommand } from \"citty\";\nimport { join } from \"pathe\";\n\nimport { loadConfig } from \"../../core/config.ts\";\nimport {\n resolveProjectContext,\n resolveRuntimeDir,\n} from \"../../core/project.ts\";\nimport { commandMeta } from \"../command-meta.ts\";\nimport { readDevLock } from \"../dev-lock.ts\";\nimport { logger } from \"../log.ts\";\nimport { prepareProject } from \"../prepare.ts\";\n\nexport const syncCommand = defineCommand({\n args: {\n force: {\n description: \"Clear the source cache before refetching.\",\n type: \"boolean\",\n },\n preview: {\n description: \"Include drafts and unpublished CMS content.\",\n type: \"boolean\",\n },\n strict: { description: \"Fail on diagnostics.\", type: \"boolean\" },\n },\n meta: commandMeta.sync,\n async run({ args }) {\n const root = process.cwd();\n\n // `--force` drops the snapshots so a stale or corrupt cache can't be served;\n // scoped to `.blume/cache`, so a running dev server's runtime is untouched.\n if (args.force) {\n const { config } = await loadConfig(root);\n const context = resolveProjectContext(root, config);\n await rm(join(context.outDir, \"cache\"), { force: true, recursive: true });\n logger.info(\"Cleared source cache.\");\n }\n\n // Dev mode keeps drafts and skips the static-build gate; `refresh` forces\n // remote sources to re-fetch rather than serve their cached snapshot. A\n // running dev server hot-reloads from the regenerated runtime — so when one\n // is live, regenerate with the same site fallback (its URL) it used, else\n // its astro.config loses `site` (dropping OG and canonicals) and Astro\n // restarts cold.\n const lock = readDevLock(resolveRuntimeDir(root));\n const devServerUrl = lock?.port\n ? `http://localhost:${lock.port}`\n : undefined;\n await prepareProject({\n devServerUrl,\n mode: \"dev\",\n preview: args.preview,\n refresh: true,\n root,\n strict: args.strict,\n });\n\n logger.success(\"Synced content sources.\");\n },\n});\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAEA;AACA;AAYO,IAAM,cAAc,cAAc;AAAA,EACvC,MAAM;AAAA,IACJ,OAAO;AAAA,MACL,aAAa;AAAA,MACb,MAAM;AAAA,IACR;AAAA,IACA,SAAS;AAAA,MACP,aAAa;AAAA,MACb,MAAM;AAAA,IACR;AAAA,IACA,QAAQ,EAAE,aAAa,wBAAwB,MAAM,UAAU;AAAA,EACjE;AAAA,EACA,MAAM,YAAY;AAAA,OACZ,IAAG,GAAG,QAAQ;AAAA,IAClB,MAAM,OAAO,QAAQ,IAAI;AAAA,IAIzB,IAAI,KAAK,OAAO;AAAA,MACd,QAAQ,WAAW,MAAM,WAAW,IAAI;AAAA,MACxC,MAAM,UAAU,sBAAsB,MAAM,MAAM;AAAA,MAClD,MAAM,GAAG,KAAK,QAAQ,QAAQ,OAAO,GAAG,EAAE,OAAO,MAAM,WAAW,KAAK,CAAC;AAAA,MACxE,OAAO,KAAK,uBAAuB;AAAA,IACrC;AAAA,IAQA,MAAM,OAAO,YAAY,kBAAkB,IAAI,CAAC;AAAA,IAChD,MAAM,eAAe,MAAM,OACvB,oBAAoB,KAAK,SACzB;AAAA,IACJ,MAAM,eAAe;AAAA,MACnB;AAAA,MACA,MAAM;AAAA,MACN,SAAS,KAAK;AAAA,MACd,SAAS;AAAA,MACT;AAAA,MACA,QAAQ,KAAK;AAAA,IACf,CAAC;AAAA,IAED,OAAO,QAAQ,yBAAyB;AAAA;AAE5C,CAAC;",
8
+ "debugId": "CD2123C1322DDD3C64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/report-format.ts
4
+ var seconds = (ms) => `${(ms / 1000).toFixed(1)}s`;
5
+ var money = (cost) => cost === undefined ? "" : `$${cost.toFixed(2)}`;
6
+ var duration = (ms) => {
7
+ if (ms < 60000) {
8
+ return seconds(ms);
9
+ }
10
+ const minutes = Math.floor(ms / 60000);
11
+ const rest = Math.round(ms % 60000 / 1000);
12
+ return `${minutes}m ${rest}s`;
13
+ };
14
+
15
+ // src/eval/agents.ts
16
+ import { readFile, writeFile } from "node:fs/promises";
17
+ import spawn from "cross-spawn";
18
+ import { join } from "pathe";
19
+ import { z } from "zod";
20
+ var KILL_GRACE_MS = 5000;
21
+ var MCP_TOOL_NAMES = [
22
+ "search_docs",
23
+ "get_page",
24
+ "list_pages",
25
+ "get_navigation"
26
+ ];
27
+ var MCP_SERVER_NAME = "docs";
28
+ var DISALLOWED_TOOLS = [
29
+ "Bash",
30
+ "Read",
31
+ "Glob",
32
+ "Grep",
33
+ "Write",
34
+ "Edit",
35
+ "NotebookEdit",
36
+ "WebFetch",
37
+ "WebSearch",
38
+ "Task"
39
+ ];
40
+ var runAgentHeadless = (bin, args, options) => new Promise((resolve, reject) => {
41
+ const child = spawn(bin, args, {
42
+ cwd: options.cwd,
43
+ stdio: ["pipe", "pipe", "pipe"]
44
+ });
45
+ let stdout = "";
46
+ let stderr = "";
47
+ let timedOut = false;
48
+ child.stdout?.on("data", (chunk) => {
49
+ stdout += chunk.toString("utf-8");
50
+ });
51
+ child.stderr?.on("data", (chunk) => {
52
+ stderr += chunk.toString("utf-8");
53
+ });
54
+ const deadline = setTimeout(() => {
55
+ timedOut = true;
56
+ child.kill("SIGTERM");
57
+ const hardKill = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
58
+ hardKill.unref();
59
+ }, options.timeoutMs);
60
+ deadline.unref();
61
+ child.once("error", (error) => {
62
+ clearTimeout(deadline);
63
+ reject(error);
64
+ });
65
+ child.once("close", (code) => {
66
+ clearTimeout(deadline);
67
+ resolve({ code: code ?? 1, stderr, stdout, timedOut });
68
+ });
69
+ child.once("exit", (code) => {
70
+ if (timedOut) {
71
+ clearTimeout(deadline);
72
+ resolve({ code: code ?? 1, stderr, stdout, timedOut });
73
+ }
74
+ });
75
+ child.stdin?.end(options.prompt);
76
+ });
77
+ var writeMcpConfig = async (dir, snapshotPath, launcher) => {
78
+ const resolved = launcher ?? {
79
+ args: [process.argv[1] ?? "", "mcp-stdio", "--data", snapshotPath],
80
+ command: process.execPath
81
+ };
82
+ const configPath = join(dir, "mcp-config.json");
83
+ const config = {
84
+ mcpServers: {
85
+ [MCP_SERVER_NAME]: { args: resolved.args, command: resolved.command }
86
+ }
87
+ };
88
+ await writeFile(configPath, JSON.stringify(config, null, 2));
89
+ return {
90
+ configPath,
91
+ serverArgs: resolved.args,
92
+ serverCommand: resolved.command
93
+ };
94
+ };
95
+ var CLAUDE_READER_MAX_TURNS = "25";
96
+ var CLAUDE_JUDGE_MAX_TURNS = "1";
97
+ var claudeArgs = (context) => {
98
+ const base = ["-p", "--output-format", "json", "--strict-mcp-config"];
99
+ if (context.mcp) {
100
+ const allowed = MCP_TOOL_NAMES.map((tool) => `mcp__${MCP_SERVER_NAME}__${tool}`).join(",");
101
+ return [
102
+ ...base,
103
+ "--mcp-config",
104
+ context.mcp.configPath,
105
+ "--allowedTools",
106
+ allowed,
107
+ "--disallowedTools",
108
+ DISALLOWED_TOOLS.join(","),
109
+ "--max-turns",
110
+ CLAUDE_READER_MAX_TURNS
111
+ ];
112
+ }
113
+ return [
114
+ ...base,
115
+ "--disallowedTools",
116
+ DISALLOWED_TOOLS.join(","),
117
+ "--max-turns",
118
+ CLAUDE_JUDGE_MAX_TURNS
119
+ ];
120
+ };
121
+ var codexArgs = (context) => {
122
+ const base = [
123
+ "exec",
124
+ "--skip-git-repo-check",
125
+ "--ignore-user-config",
126
+ "--ephemeral",
127
+ "--sandbox",
128
+ "read-only",
129
+ "--output-last-message",
130
+ context.lastMessagePath
131
+ ];
132
+ if (context.mcp) {
133
+ return [
134
+ ...base,
135
+ "-c",
136
+ `mcp_servers.${MCP_SERVER_NAME}.command=${JSON.stringify(context.mcp.serverCommand)}`,
137
+ "-c",
138
+ `mcp_servers.${MCP_SERVER_NAME}.args=${JSON.stringify(context.mcp.serverArgs)}`,
139
+ "-"
140
+ ];
141
+ }
142
+ return [...base, "-"];
143
+ };
144
+ var agentArgs = (kind, context) => kind === "claude" ? claudeArgs(context) : codexArgs(context);
145
+ var claudeResultSchema = z.object({
146
+ is_error: z.boolean().default(false),
147
+ result: z.string().default(""),
148
+ total_cost_usd: z.number().optional()
149
+ });
150
+ var tail = (value, max = 300) => {
151
+ const trimmed = value.trim();
152
+ return trimmed.length > max ? trimmed.slice(-max) : trimmed;
153
+ };
154
+ var readAgentOutput = async (kind, result, lastMessagePath) => {
155
+ if (result.timedOut) {
156
+ return { detail: "timed out", isError: true, text: "" };
157
+ }
158
+ if (result.code !== 0) {
159
+ return {
160
+ detail: tail(result.stderr) || `exited with code ${result.code}`,
161
+ isError: true,
162
+ text: ""
163
+ };
164
+ }
165
+ if (kind === "claude") {
166
+ let parsed;
167
+ try {
168
+ parsed = JSON.parse(result.stdout);
169
+ } catch {
170
+ return {
171
+ detail: "unparseable --output-format json payload",
172
+ isError: true,
173
+ text: ""
174
+ };
175
+ }
176
+ const payload = claudeResultSchema.safeParse(parsed);
177
+ if (!payload.success) {
178
+ return {
179
+ detail: "unexpected --output-format json shape",
180
+ isError: true,
181
+ text: ""
182
+ };
183
+ }
184
+ return {
185
+ costUsd: payload.data.total_cost_usd,
186
+ isError: payload.data.is_error,
187
+ text: payload.data.result
188
+ };
189
+ }
190
+ let text;
191
+ try {
192
+ const raw = await readFile(lastMessagePath, "utf-8");
193
+ text = raw.trim();
194
+ } catch {
195
+ return { detail: "no last message written", isError: true, text: "" };
196
+ }
197
+ if (text === "") {
198
+ return { detail: "empty last message", isError: true, text: "" };
199
+ }
200
+ return { isError: false, text };
201
+ };
202
+ var verdictSchema = z.object({
203
+ missing: z.array(z.string()).default([]),
204
+ notes: z.string().default(""),
205
+ pass: z.boolean(),
206
+ score: z.number().min(0).max(1).optional()
207
+ });
208
+ var parseVerdict = (text) => {
209
+ const start = text.indexOf("{");
210
+ const end = text.lastIndexOf("}");
211
+ if (start === -1 || end <= start) {
212
+ return;
213
+ }
214
+ let parsed;
215
+ try {
216
+ parsed = JSON.parse(text.slice(start, end + 1));
217
+ } catch {
218
+ return;
219
+ }
220
+ const result = verdictSchema.safeParse(parsed);
221
+ return result.success ? result.data : undefined;
222
+ };
223
+
224
+ export { seconds, money, duration, DISALLOWED_TOOLS, runAgentHeadless, writeMcpConfig, agentArgs, readAgentOutput, parseVerdict };
225
+
226
+ //# debugId=4B8025B7FB4CE67464756E2164756E21
227
+ //# sourceMappingURL=chunk-s5e5jt53.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/cli/report-format.ts", "../src/eval/agents.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Number formatting shared by the translate and eval report renderers. The\n * `1.5s` / `4m 12s` shapes are asserted in both suites — change them in both\n * minds at once.\n */\n\n/** Milliseconds as `1.5s`. */\nexport const seconds = (ms: number): string => `${(ms / 1000).toFixed(1)}s`;\n\n/** A dollar cost as `$1.23`, or nothing when the run reported none. */\nexport const money = (cost: number | undefined): string =>\n cost === undefined ? \"\" : `$${cost.toFixed(2)}`;\n\n/** Milliseconds as `12.3s` under a minute, `4m 12s` from there up. */\nexport const duration = (ms: number): string => {\n if (ms < 60_000) {\n return seconds(ms);\n }\n const minutes = Math.floor(ms / 60_000);\n const rest = Math.round((ms % 60_000) / 1000);\n return `${minutes}m ${rest}s`;\n};\n",
6
+ "import { readFile, writeFile } from \"node:fs/promises\";\n\nimport spawn from \"cross-spawn\";\nimport { join } from \"pathe\";\nimport { z } from \"zod\";\n\nimport type { AgentKind } from \"../audit/agent.ts\";\n\n/** How long the SIGTERM on timeout gets to work before SIGKILL follows. */\nconst KILL_GRACE_MS = 5000;\n\n/** The MCP tools a reader run may use — nothing else. */\nexport const MCP_TOOL_NAMES = [\n \"search_docs\",\n \"get_page\",\n \"list_pages\",\n \"get_navigation\",\n] as const;\n\n/** The MCP server name in the generated config; tool ids derive from it. */\nconst MCP_SERVER_NAME = \"docs\";\n\n/**\n * Claude Code built-ins that would let the agent escape the docs-only\n * sandbox: the reader must not read the repo, run commands, or search the\n * web — it sees the documentation the way a stranger does, through MCP.\n */\nexport const DISALLOWED_TOOLS = [\n \"Bash\",\n \"Read\",\n \"Glob\",\n \"Grep\",\n \"Write\",\n \"Edit\",\n \"NotebookEdit\",\n \"WebFetch\",\n \"WebSearch\",\n \"Task\",\n];\n\n/** The captured outcome of one headless agent invocation. */\nexport interface HeadlessResult {\n code: number;\n stderr: string;\n stdout: string;\n timedOut: boolean;\n}\n\nexport interface HeadlessOptions {\n cwd: string;\n prompt: string;\n timeoutMs: number;\n}\n\n/**\n * Run an agent CLI headlessly: prompt over stdin (dodging argv limits and\n * cmd.exe newline quoting alike), stdout and stderr captured, SIGTERM at the\n * deadline with a SIGKILL follow-up. Resolves with the captured result;\n * rejects only when the executable cannot be spawned at all (ENOENT).\n *\n * Spawned through cross-spawn, which runs npm's `.cmd` shims on Windows with\n * its own cmd.exe escaping — under the old `shell: true` launch, cmd.exe was\n * free to mangle the JSON carried by codex's `-c` arguments, and a missing\n * executable surfaced as exit code 9009 instead of an ENOENT rejection.\n * execa was evaluated and rejected here: it waits for the stdio streams to\n * close, and the `exit`-beats-`close` resolution below exists precisely\n * because a killed agent's own children (an MCP server, a shell) hold the\n * pipes open past the SIGTERM.\n */\nexport const runAgentHeadless = (\n bin: string,\n args: string[],\n options: HeadlessOptions\n): Promise<HeadlessResult> =>\n // oxlint-disable-next-line promise/avoid-new -- adapt spawn's event callbacks\n new Promise((resolve, reject) => {\n const child = spawn(bin, args, {\n cwd: options.cwd,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n let stdout = \"\";\n let stderr = \"\";\n let timedOut = false;\n child.stdout?.on(\"data\", (chunk: Buffer) => {\n stdout += chunk.toString(\"utf-8\");\n });\n child.stderr?.on(\"data\", (chunk: Buffer) => {\n stderr += chunk.toString(\"utf-8\");\n });\n\n const deadline = setTimeout(() => {\n timedOut = true;\n child.kill(\"SIGTERM\");\n const hardKill = setTimeout(() => child.kill(\"SIGKILL\"), KILL_GRACE_MS);\n hardKill.unref();\n }, options.timeoutMs);\n deadline.unref();\n\n child.once(\"error\", (error) => {\n clearTimeout(deadline);\n reject(error);\n });\n child.once(\"close\", (code) => {\n clearTimeout(deadline);\n resolve({ code: code ?? 1, stderr, stdout, timedOut });\n });\n // `close` waits for the stdio pipes, which a killed agent's own children\n // (an MCP server, a shell) can hold open past the SIGTERM. A timed-out\n // run's output is discarded anyway, so the process dying is enough.\n child.once(\"exit\", (code) => {\n if (timedOut) {\n clearTimeout(deadline);\n resolve({ code: code ?? 1, stderr, stdout, timedOut });\n }\n });\n\n child.stdin?.end(options.prompt);\n });\n\n/** The spawn signature `runEval` accepts, injectable for tests. */\nexport type HeadlessRunner = typeof runAgentHeadless;\n\n/** How the eval reaches the MCP stdio bridge from a spawned agent. */\nexport interface McpLaunch {\n /** The generated MCP config file (claude's `--mcp-config`). */\n configPath: string;\n /** argv for the bridge process (codex's `-c mcp_servers` override). */\n serverArgs: string[];\n /** The executable launching the bridge. */\n serverCommand: string;\n}\n\n/**\n * Write the MCP config a reader run points its agent CLI at. The bridge is\n * this same CLI relaunched (`blume mcp-stdio`), which resolves correctly from\n * both a source checkout (bun + src/cli/index.ts) and an installed package\n * (node + bin/blume.mjs).\n */\nexport const writeMcpConfig = async (\n dir: string,\n snapshotPath: string,\n launcher?: { args: string[]; command: string }\n): Promise<McpLaunch> => {\n const resolved = launcher ?? {\n args: [process.argv[1] ?? \"\", \"mcp-stdio\", \"--data\", snapshotPath],\n command: process.execPath,\n };\n const configPath = join(dir, \"mcp-config.json\");\n const config = {\n mcpServers: {\n [MCP_SERVER_NAME]: { args: resolved.args, command: resolved.command },\n },\n };\n await writeFile(configPath, JSON.stringify(config, null, 2));\n return {\n configPath,\n serverArgs: resolved.args,\n serverCommand: resolved.command,\n };\n};\n\nexport interface InvocationContext {\n /** Where codex writes its final message; unused by claude. */\n lastMessagePath: string;\n /** The MCP bridge for reader runs; omitted for the judge. */\n mcp?: McpLaunch;\n}\n\nconst CLAUDE_READER_MAX_TURNS = \"25\";\nconst CLAUDE_JUDGE_MAX_TURNS = \"1\";\n\nconst claudeArgs = (context: InvocationContext): string[] => {\n const base = [\"-p\", \"--output-format\", \"json\", \"--strict-mcp-config\"];\n if (context.mcp) {\n const allowed = MCP_TOOL_NAMES.map(\n (tool) => `mcp__${MCP_SERVER_NAME}__${tool}`\n ).join(\",\");\n return [\n ...base,\n \"--mcp-config\",\n context.mcp.configPath,\n \"--allowedTools\",\n allowed,\n \"--disallowedTools\",\n DISALLOWED_TOOLS.join(\",\"),\n \"--max-turns\",\n CLAUDE_READER_MAX_TURNS,\n ];\n }\n return [\n ...base,\n \"--disallowedTools\",\n DISALLOWED_TOOLS.join(\",\"),\n \"--max-turns\",\n CLAUDE_JUDGE_MAX_TURNS,\n ];\n};\n\nconst codexArgs = (context: InvocationContext): string[] => {\n const base = [\n \"exec\",\n \"--skip-git-repo-check\",\n \"--ignore-user-config\",\n \"--ephemeral\",\n \"--sandbox\",\n \"read-only\",\n \"--output-last-message\",\n context.lastMessagePath,\n ];\n if (context.mcp) {\n // `-c` values parse as TOML; JSON string/array literals are valid TOML\n // values, so JSON.stringify produces exactly the quoting codex expects.\n return [\n ...base,\n \"-c\",\n `mcp_servers.${MCP_SERVER_NAME}.command=${JSON.stringify(\n context.mcp.serverCommand\n )}`,\n \"-c\",\n `mcp_servers.${MCP_SERVER_NAME}.args=${JSON.stringify(\n context.mcp.serverArgs\n )}`,\n \"-\",\n ];\n }\n return [...base, \"-\"];\n};\n\n/** Build the argv for one headless run; pass `mcp` for the reader role. */\nexport const agentArgs = (\n kind: AgentKind,\n context: InvocationContext\n): string[] => (kind === \"claude\" ? claudeArgs(context) : codexArgs(context));\n\n/** What one headless run produced, normalized across agent CLIs. */\nexport interface AgentOutput {\n costUsd?: number;\n detail?: string;\n isError: boolean;\n text: string;\n}\n\nconst claudeResultSchema = z.object({\n is_error: z.boolean().default(false),\n result: z.string().default(\"\"),\n total_cost_usd: z.number().optional(),\n});\n\nconst tail = (value: string, max = 300): string => {\n const trimmed = value.trim();\n return trimmed.length > max ? trimmed.slice(-max) : trimmed;\n};\n\n/**\n * Normalize a finished run into the answer text. Claude prints one JSON\n * object on stdout (`--output-format json`); codex writes the final message\n * to `--output-last-message` because its stdout interleaves progress.\n */\nexport const readAgentOutput = async (\n kind: AgentKind,\n result: HeadlessResult,\n lastMessagePath: string\n): Promise<AgentOutput> => {\n if (result.timedOut) {\n return { detail: \"timed out\", isError: true, text: \"\" };\n }\n if (result.code !== 0) {\n return {\n detail: tail(result.stderr) || `exited with code ${result.code}`,\n isError: true,\n text: \"\",\n };\n }\n\n if (kind === \"claude\") {\n let parsed: unknown;\n try {\n parsed = JSON.parse(result.stdout);\n } catch {\n return {\n detail: \"unparseable --output-format json payload\",\n isError: true,\n text: \"\",\n };\n }\n const payload = claudeResultSchema.safeParse(parsed);\n if (!payload.success) {\n return {\n detail: \"unexpected --output-format json shape\",\n isError: true,\n text: \"\",\n };\n }\n return {\n costUsd: payload.data.total_cost_usd,\n isError: payload.data.is_error,\n text: payload.data.result,\n };\n }\n\n let text: string;\n try {\n const raw = await readFile(lastMessagePath, \"utf-8\");\n text = raw.trim();\n } catch {\n return { detail: \"no last message written\", isError: true, text: \"\" };\n }\n if (text === \"\") {\n return { detail: \"empty last message\", isError: true, text: \"\" };\n }\n return { isError: false, text };\n};\n\nconst verdictSchema = z.object({\n missing: z.array(z.string()).default([]),\n notes: z.string().default(\"\"),\n pass: z.boolean(),\n score: z.number().min(0).max(1).optional(),\n});\n\n/** The judge's grade for one answer. */\nexport type Verdict = z.infer<typeof verdictSchema>;\n\n/**\n * Extract the verdict JSON from a judge reply. Tolerates markdown fences and\n * surrounding prose; returns undefined when no valid verdict can be found.\n */\nexport const parseVerdict = (text: string): Verdict | undefined => {\n const start = text.indexOf(\"{\");\n const end = text.lastIndexOf(\"}\");\n if (start === -1 || end <= start) {\n return;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(text.slice(start, end + 1));\n } catch {\n return;\n }\n const result = verdictSchema.safeParse(parsed);\n return result.success ? result.data : undefined;\n};\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "4B8025B7FB4CE67464756E2164756E21",
10
+ "names": []
11
+ }
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/astro/pages.ts
4
+ import { extname, relative } from "pathe";
5
+ import { glob, globSync } from "tinyglobby";
6
+ var PAGE_GLOB = ["**/*.astro"];
7
+ var isPrivatePage = (rel) => rel.split("/").some((segment) => segment.startsWith("_"));
8
+ var toPageRoutes = (pagesRoot, files) => {
9
+ files.sort();
10
+ const routes = [];
11
+ for (const file of files) {
12
+ const rel = relative(pagesRoot, file);
13
+ if (isPrivatePage(rel)) {
14
+ continue;
15
+ }
16
+ const withoutExt = rel.slice(0, rel.length - extname(rel).length);
17
+ const parts = withoutExt.split("/");
18
+ if (parts.at(-1) === "index") {
19
+ parts.pop();
20
+ }
21
+ const pattern = parts.length === 0 ? "/" : `/${parts.join("/")}`;
22
+ routes.push({ entrypoint: file, pattern });
23
+ }
24
+ return routes;
25
+ };
26
+ var discoverPages = async (pagesRoot) => toPageRoutes(pagesRoot, await glob(PAGE_GLOB, { absolute: true, cwd: pagesRoot, onlyFiles: true }));
27
+ var routeIsTaken = (pages, contentPages, route) => pages.some((page) => page.pattern === route) || contentPages.some((page) => page.route === route);
28
+ var PRIVATE_SEGMENT = /^[._]/u;
29
+ var staticSegments = (pattern) => {
30
+ const segments = pattern.split("/").filter(Boolean);
31
+ return segments.some((part) => PRIVATE_SEGMENT.test(part) || part.includes("[")) ? null : segments;
32
+ };
33
+ var customStaticRoutes = (pages) => {
34
+ const routes = new Set;
35
+ for (const { pattern } of pages) {
36
+ const segments = staticSegments(pattern);
37
+ if (segments !== null) {
38
+ routes.add(segments.length === 0 ? "/" : `/${segments.join("/")}`);
39
+ }
40
+ }
41
+ return [...routes];
42
+ };
43
+ var hasGeneratedChangelog = (project, userPages) => {
44
+ const hasChangelog = project.graph.pages.some((page) => page.contentType === "changelog" && !(page.meta.draft || page.meta.sidebar.hidden));
45
+ const hasChangelogSource = (project.config.content.sources ?? []).some((source) => source.type === "github-releases");
46
+ return (hasChangelog || hasChangelogSource) && !routeIsTaken(userPages, project.graph.pages, "/changelog");
47
+ };
48
+ var humanizeSegment = (segment) => segment.split(/[-_]/u).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
49
+ var customOgRoutes = (pages, siteTitle, titles = {}) => {
50
+ const overrides = new Map(Object.entries(titles).map(([route, title]) => [
51
+ `/${route.split("/").filter(Boolean).join("/")}`,
52
+ title
53
+ ]));
54
+ const seen = new Set;
55
+ const routes = [];
56
+ const collectRoute = (pattern) => {
57
+ const segments = staticSegments(pattern);
58
+ if (segments === null) {
59
+ return;
60
+ }
61
+ const slug = segments.length === 0 ? "index" : segments.join("/");
62
+ if (seen.has(slug)) {
63
+ return;
64
+ }
65
+ seen.add(slug);
66
+ const last = segments.at(-1);
67
+ routes.push({
68
+ slug,
69
+ title: overrides.get(`/${segments.join("/")}`) ?? (last ? humanizeSegment(last) : siteTitle)
70
+ });
71
+ };
72
+ for (const { pattern } of pages) {
73
+ collectRoute(pattern);
74
+ }
75
+ return routes;
76
+ };
77
+
78
+ export { discoverPages, routeIsTaken, customStaticRoutes, hasGeneratedChangelog, customOgRoutes };
79
+
80
+ //# debugId=80BD00D65DB37D4C64756E2164756E21
81
+ //# sourceMappingURL=chunk-sbdqrjbb.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/astro/pages.ts"],
4
+ "sourcesContent": [
5
+ "import { extname, relative } from \"pathe\";\nimport { glob, globSync } from \"tinyglobby\";\n\nimport type { BlumeProject } from \"../core/project-graph.ts\";\nimport type { BlumePageRoute } from \"./integration.ts\";\n\nconst PAGE_GLOB = [\"**/*.astro\"];\n\n/**\n * Astro's routing convention: a file or folder whose name starts with `_` is a\n * private partial — importable (shared layouts, home-page sections), but never\n * built into a route. Blume injects pages itself, so it must reproduce the same\n * exclusion or every `pages/_home/Hero.astro`-style component ships as an HTML\n * page.\n */\nconst isPrivatePage = (rel: string): boolean =>\n rel.split(\"/\").some((segment) => segment.startsWith(\"_\"));\n\n/** Map discovered page files to routes; shared by the async/sync discoverers. */\nconst toPageRoutes = (pagesRoot: string, files: string[]): BlumePageRoute[] => {\n files.sort();\n const routes: BlumePageRoute[] = [];\n for (const file of files) {\n const rel = relative(pagesRoot, file);\n if (isPrivatePage(rel)) {\n continue;\n }\n const withoutExt = rel.slice(0, rel.length - extname(rel).length);\n const parts = withoutExt.split(\"/\");\n // Only a trailing `index` maps to its parent dir; a folder literally named\n // `index` (e.g. `index/foo.astro`) must keep its segment.\n if (parts.at(-1) === \"index\") {\n parts.pop();\n }\n const pattern = parts.length === 0 ? \"/\" : `/${parts.join(\"/\")}`;\n routes.push({ entrypoint: file, pattern });\n }\n return routes;\n};\n\n/**\n * Discover user `.astro` pages and map them to route patterns. Files keep their\n * original location; only the route pattern is derived (index -> parent,\n * dynamic `[param]` segments preserved).\n */\nexport const discoverPages = async (\n pagesRoot: string\n): Promise<BlumePageRoute[]> =>\n toPageRoutes(\n pagesRoot,\n await glob(PAGE_GLOB, { absolute: true, cwd: pagesRoot, onlyFiles: true })\n );\n\n/** {@link discoverPages} for synchronous callers (e.g. the sitemap builder). */\nexport const discoverPagesSync = (pagesRoot: string): BlumePageRoute[] =>\n toPageRoutes(\n pagesRoot,\n globSync(PAGE_GLOB, { absolute: true, cwd: pagesRoot, onlyFiles: true })\n );\n\n/**\n * Whether the project already owns `route` — through a custom `.astro` page\n * (injected, so matched on `pattern`) or a content page (matched on `route`).\n * Used to skip a generated default page (e.g. `/404`, `/changelog`) so a\n * user-authored page overrides it without a route collision.\n */\nexport const routeIsTaken = (\n pages: { pattern: string }[],\n contentPages: { route: string }[],\n route: string\n): boolean =>\n pages.some((page) => page.pattern === route) ||\n contentPages.some((page) => page.route === route);\n\n/** A custom-page route that should get a generated OG card. */\nexport interface OgCustomRoute {\n /** `og/<slug>.png` path segment; `index` for the site root. */\n slug: string;\n /** Card headline. */\n title: string;\n}\n\n/** Skip private (`_partial`, `.well-known`) and Astro dynamic (`[param]`) parts. */\nconst PRIVATE_SEGMENT = /^[._]/u;\n\n/** Segments of a static, shareable page pattern; null for dynamic/private ones. */\nconst staticSegments = (pattern: string): string[] | null => {\n const segments = pattern.split(\"/\").filter(Boolean);\n return segments.some(\n (part) => PRIVATE_SEGMENT.test(part) || part.includes(\"[\")\n )\n ? null\n : segments;\n};\n\n/**\n * The static routes served by custom `.astro` pages — the same filtering as\n * {@link customOgRoutes}, but yielding the routes themselves. Feeds the route\n * sets that must know every servable page beyond the content graph (the link\n * checker, the sitemap); dynamic (`[param]`) and private segments are skipped\n * because their concrete URLs can't be enumerated statically.\n */\nexport const customStaticRoutes = (pages: { pattern: string }[]): string[] => {\n const routes = new Set<string>();\n for (const { pattern } of pages) {\n const segments = staticSegments(pattern);\n if (segments !== null) {\n routes.add(segments.length === 0 ? \"/\" : `/${segments.join(\"/\")}`);\n }\n }\n return [...routes];\n};\n\n/**\n * Whether the generated `/changelog` index route exists for this project —\n * `generate.ts` (which writes the page) and the sitemap/link validator all\n * share this check: there are visible `type: changelog` entries — or a\n * release-backed changelog source, whose route must resolve even when a fetch\n * fails — and no user content or custom page already owns `/changelog`.\n */\nexport const hasGeneratedChangelog = (\n project: BlumeProject,\n userPages: { pattern: string }[]\n): boolean => {\n const hasChangelog = project.graph.pages.some(\n (page) =>\n page.contentType === \"changelog\" &&\n !(page.meta.draft || page.meta.sidebar.hidden)\n );\n const hasChangelogSource = (project.config.content.sources ?? []).some(\n (source) => source.type === \"github-releases\"\n );\n return (\n (hasChangelog || hasChangelogSource) &&\n !routeIsTaken(userPages, project.graph.pages, \"/changelog\")\n );\n};\n\nconst humanizeSegment = (segment: string): string =>\n segment\n .split(/[-_]/u)\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(\" \");\n\n/**\n * Pick the custom-page routes that should get a generated Open Graph card, with\n * the card's slug and text. OG is otherwise content-route only, so a custom page\n * — most importantly the landing `/`, the most-shared URL — would have no card.\n *\n * Dynamic (`[param]`) routes and private segments (`_partials`, `.well-known`)\n * are skipped: they aren't shareable pages. A `titles` entry (`seo.og.titles`,\n * keyed by route) names a card outright — the only way to say \"CLI\" when the\n * segment humanizes to \"Cli\". Otherwise the home is titled with the site title\n * and a deeper page from its last path segment. The card's brand lockup,\n * description, and footer come from the resolved config at render time.\n */\nexport const customOgRoutes = (\n pages: BlumePageRoute[],\n siteTitle: string,\n titles: Record<string, string> = {}\n): OgCustomRoute[] => {\n // Keys normalized to `/`-joined segments so `cli`, `/cli`, and `/cli/` all\n // address the page served at `/cli` (and `/` addresses the home).\n const overrides = new Map(\n Object.entries(titles).map(([route, title]) => [\n `/${route.split(\"/\").filter(Boolean).join(\"/\")}`,\n title,\n ])\n );\n const seen = new Set<string>();\n const routes: OgCustomRoute[] = [];\n // Extracted so the skip paths become early `return`s (one `continue` budget\n // per loop under the lint rule) instead of `continue` statements.\n const collectRoute = (pattern: string): void => {\n const segments = staticSegments(pattern);\n if (segments === null) {\n return;\n }\n const slug = segments.length === 0 ? \"index\" : segments.join(\"/\");\n if (seen.has(slug)) {\n return;\n }\n seen.add(slug);\n const last = segments.at(-1);\n routes.push({\n slug,\n title:\n overrides.get(`/${segments.join(\"/\")}`) ??\n (last ? humanizeSegment(last) : siteTitle),\n });\n };\n for (const { pattern } of pages) {\n collectRoute(pattern);\n }\n return routes;\n};\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "80BD00D65DB37D4C64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ resolveRuntimeDir
4
+ } from "./chunk-vv3f8mb6.js";
5
+ import {
6
+ logger
7
+ } from "./chunk-ey89bjj1.js";
8
+
9
+ // src/cli/dev-lock.ts
10
+ import {
11
+ existsSync,
12
+ mkdirSync,
13
+ readFileSync,
14
+ rmSync,
15
+ writeFileSync
16
+ } from "node:fs";
17
+ import { join } from "pathe";
18
+ var lockPath = (outDir) => join(outDir, "dev.lock");
19
+ var isValidPid = (pid) => typeof pid === "number" && Number.isInteger(pid) && pid > 0;
20
+ var isPortNumber = (port) => typeof port === "number";
21
+ var isLockRecord = (data) => typeof data === "object" && data !== null;
22
+ var parseLock = (raw) => {
23
+ let data;
24
+ try {
25
+ data = JSON.parse(raw.trim());
26
+ } catch {
27
+ return null;
28
+ }
29
+ if (isValidPid(data)) {
30
+ return { pid: data };
31
+ }
32
+ if (isLockRecord(data)) {
33
+ const { pid, port } = data;
34
+ if (isValidPid(pid)) {
35
+ return isPortNumber(port) ? { pid, port } : { pid };
36
+ }
37
+ }
38
+ return null;
39
+ };
40
+ var isProcessAlive = (pid) => {
41
+ try {
42
+ process.kill(pid, 0);
43
+ return true;
44
+ } catch (error) {
45
+ return error.code === "EPERM";
46
+ }
47
+ };
48
+ var readDevLock = (outDir) => {
49
+ const path = lockPath(outDir);
50
+ if (!existsSync(path)) {
51
+ return null;
52
+ }
53
+ const lock = parseLock(readFileSync(path, "utf-8"));
54
+ return lock && isProcessAlive(lock.pid) ? lock : null;
55
+ };
56
+ var lockPayload = (port) => {
57
+ const info = { pid: process.pid };
58
+ if (port !== undefined) {
59
+ info.port = port;
60
+ }
61
+ return JSON.stringify(info);
62
+ };
63
+ var writeLock = (outDir, port) => {
64
+ writeFileSync(lockPath(outDir), lockPayload(port));
65
+ };
66
+
67
+ class DevLockHeldError extends Error {
68
+ lock;
69
+ constructor(lock) {
70
+ super(`A blume dev server (pid ${lock.pid}) already holds the lock.`);
71
+ this.name = "DevLockHeldError";
72
+ this.lock = lock;
73
+ }
74
+ }
75
+ var ownsLock = (outDir) => {
76
+ const path = lockPath(outDir);
77
+ if (!existsSync(path)) {
78
+ return false;
79
+ }
80
+ try {
81
+ return parseLock(readFileSync(path, "utf-8"))?.pid === process.pid;
82
+ } catch {
83
+ return false;
84
+ }
85
+ };
86
+ var tryClaimLock = (outDir, port) => {
87
+ try {
88
+ writeFileSync(lockPath(outDir), lockPayload(port), { flag: "wx" });
89
+ return true;
90
+ } catch (error) {
91
+ if (error.code !== "EEXIST") {
92
+ throw error;
93
+ }
94
+ const existing = readDevLock(outDir);
95
+ if (existing && existing.pid !== process.pid) {
96
+ throw new DevLockHeldError(existing);
97
+ }
98
+ rmSync(lockPath(outDir), { force: true });
99
+ return false;
100
+ }
101
+ };
102
+ var acquireDevLock = (outDir, port) => {
103
+ mkdirSync(outDir, { recursive: true });
104
+ while (!tryClaimLock(outDir, port)) {}
105
+ let released = false;
106
+ return () => {
107
+ if (released) {
108
+ return;
109
+ }
110
+ released = true;
111
+ try {
112
+ if (ownsLock(outDir)) {
113
+ rmSync(lockPath(outDir), { force: true });
114
+ }
115
+ } catch {}
116
+ };
117
+ };
118
+ var updateDevLockPort = (outDir, port) => {
119
+ if (ownsLock(outDir)) {
120
+ writeLock(outDir, port);
121
+ }
122
+ };
123
+ var describeDevLock = (lock) => lock.port === undefined ? "" : ` at http://localhost:${lock.port}`;
124
+ var refuseIfDevRunning = (root, action, options = {}) => {
125
+ const lock = readDevLock(resolveRuntimeDir(root, options.runtimeDir));
126
+ if (lock) {
127
+ const remedies = options.isolatedHint ? "Reuse that server, stop it first, or re-run with --isolated to build/verify against .blume-verify without touching it." : "Reuse that server or stop it first.";
128
+ logger.error(`A \`blume dev\` server is running${describeDevLock(lock)}; ${action} would corrupt its .blume runtime. ${remedies}`);
129
+ process.exit(1);
130
+ }
131
+ };
132
+
133
+ export { readDevLock, DevLockHeldError, acquireDevLock, updateDevLockPort, describeDevLock, refuseIfDevRunning };
134
+
135
+ //# debugId=1007843429DAE30664756E2164756E21
136
+ //# sourceMappingURL=chunk-tc89yh2r.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/cli/dev-lock.ts"],
4
+ "sourcesContent": [
5
+ "import {\n existsSync,\n mkdirSync,\n readFileSync,\n rmSync,\n writeFileSync,\n} from \"node:fs\";\n\nimport { join } from \"pathe\";\n\nimport { resolveRuntimeDir } from \"../core/project.ts\";\nimport { logger } from \"./log.ts\";\n\n/**\n * A best-effort PID lock in the shared `.blume/` runtime dir. `blume dev`\n * regenerates and serves `.blume` continuously, so a concurrent `build`,\n * `eject`, or second `dev` that regenerates or deletes it out from under the\n * running Vite server corrupts the dev session. The lock lets those commands\n * detect a live dev server and refuse — and, because it records the server's\n * port, point the caller (often an agent that just tried to start its own\n * server) at the URL to reuse instead.\n */\n\nexport interface DevLockInfo {\n pid: number;\n /** Port the dev server is bound to, when known. */\n port?: number;\n}\n\nconst lockPath = (outDir: string): string => join(outDir, \"dev.lock\");\n\n/** What `JSON.parse` can yield for a lock file body. */\ntype LockFileValue =\n | string\n | number\n | boolean\n | null\n | LockFileValue[]\n | { [key: string]: LockFileValue };\n\nconst isValidPid = (pid: LockFileValue | undefined): pid is number =>\n typeof pid === \"number\" && Number.isInteger(pid) && pid > 0;\n\nconst isPortNumber = (port: LockFileValue | undefined): port is number =>\n typeof port === \"number\";\n\nconst isLockRecord = (\n data: LockFileValue\n): data is { pid?: LockFileValue; port?: LockFileValue } =>\n typeof data === \"object\" && data !== null;\n\n/**\n * Parse a lock file body. Current locks are JSON (`{\"pid\":123,\"port\":3001}`);\n * a bare integer (the pre-port format) still parses as a pid-only lock.\n */\nconst parseLock = (raw: string): DevLockInfo | null => {\n let data: LockFileValue;\n try {\n data = JSON.parse(raw.trim());\n } catch {\n return null;\n }\n if (isValidPid(data)) {\n return { pid: data };\n }\n if (isLockRecord(data)) {\n const { pid, port } = data;\n if (isValidPid(pid)) {\n return isPortNumber(port) ? { pid, port } : { pid };\n }\n }\n return null;\n};\n\nconst isProcessAlive = (pid: number): boolean => {\n try {\n // Signal 0 probes liveness without actually signaling the process.\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // EPERM means the process exists but belongs to another user — still\n // live, so the lock must hold (only ESRCH proves it's gone).\n // SAFETY: `process.kill` failures are errno exceptions.\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n};\n\n/**\n * Read the lock on `outDir` held by a live `blume dev`, or null. A lock left\n * by a process that has since exited (stale) is treated as absent.\n */\nexport const readDevLock = (outDir: string): DevLockInfo | null => {\n const path = lockPath(outDir);\n if (!existsSync(path)) {\n return null;\n }\n const lock = parseLock(readFileSync(path, \"utf-8\"));\n return lock && isProcessAlive(lock.pid) ? lock : null;\n};\n\n/** Whether another live `blume dev` holds the lock on `outDir`. */\nexport const isDevLocked = (outDir: string): boolean =>\n readDevLock(outDir) !== null;\n\nconst lockPayload = (port?: number): string => {\n const info: DevLockInfo = { pid: process.pid };\n if (port !== undefined) {\n info.port = port;\n }\n return JSON.stringify(info);\n};\n\nconst writeLock = (outDir: string, port?: number): void => {\n writeFileSync(lockPath(outDir), lockPayload(port));\n};\n\n/** Thrown when another live `blume dev` already holds the lock. */\nexport class DevLockHeldError extends Error {\n readonly lock: DevLockInfo;\n\n constructor(lock: DevLockInfo) {\n super(`A blume dev server (pid ${lock.pid}) already holds the lock.`);\n this.name = \"DevLockHeldError\";\n this.lock = lock;\n }\n}\n\nconst ownsLock = (outDir: string): boolean => {\n const path = lockPath(outDir);\n if (!existsSync(path)) {\n return false;\n }\n try {\n return parseLock(readFileSync(path, \"utf-8\"))?.pid === process.pid;\n } catch {\n return false;\n }\n};\n\n/**\n * One atomic claim attempt. Returns `true` when this process wins the `wx`\n * write, `false` when the dir held a stale or our-own leftover lock (now\n * cleared, so the caller should retry). Throws {@link DevLockHeldError} when a\n * live foreign holder owns the dir.\n */\nconst tryClaimLock = (outDir: string, port?: number): boolean => {\n try {\n writeFileSync(lockPath(outDir), lockPayload(port), { flag: \"wx\" });\n return true;\n } catch (error) {\n // SAFETY: `writeFileSync` failures are errno exceptions.\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") {\n throw error;\n }\n const existing = readDevLock(outDir);\n if (existing && existing.pid !== process.pid) {\n throw new DevLockHeldError(existing);\n }\n // Stale or our own leftover: clear it and race for the claim again.\n rmSync(lockPath(outDir), { force: true });\n return false;\n }\n};\n\n/**\n * Write the current process's dev lock into `outDir` and return a release\n * function. The claim is atomic (`wx`): two `blume dev` processes racing the\n * same dir can't both pass a check-then-write — the loser gets a\n * {@link DevLockHeldError} naming the live holder. A stale lock (dead pid) or\n * this process's own leftover is cleared and re-claimed. The release only\n * removes the file if it's still ours, so a newer dev server's lock is never\n * clobbered.\n */\nexport const acquireDevLock = (outDir: string, port?: number): (() => void) => {\n mkdirSync(outDir, { recursive: true });\n while (!tryClaimLock(outDir, port)) {\n // Retry until we win the claim or a live holder makes us throw.\n }\n let released = false;\n return () => {\n if (released) {\n return;\n }\n released = true;\n try {\n if (ownsLock(outDir)) {\n rmSync(lockPath(outDir), { force: true });\n }\n } catch {\n // Best-effort cleanup; a stale lock is handled by the liveness check.\n }\n };\n};\n\n/**\n * Rewrite this process's lock with the port the server actually bound (the\n * lock is acquired before the server starts, and Vite may bump a busy port).\n * A lock owned by another process is left alone.\n */\nexport const updateDevLockPort = (outDir: string, port: number): void => {\n if (ownsLock(outDir)) {\n writeLock(outDir, port);\n }\n};\n\n/** Human-readable location of a locked dev server, e.g. \" at http://localhost:3001\". */\nexport const describeDevLock = (lock: DevLockInfo): string =>\n lock.port === undefined ? \"\" : ` at http://localhost:${lock.port}`;\n\n/**\n * Exit with an error when a live `blume dev` owns the runtime dir under `root`.\n * `action` names the operation being refused (e.g. \"building\"). `runtimeDir`\n * relocates the checked dir: an isolated verify (`.blume-verify`) targets a dir\n * dev never locks, so it proceeds; a default or `--runtime-dir .blume` run still\n * refuses. Only commands that actually accept `--isolated` (build, check) should\n * set `isolatedHint`, so the refusal never suggests a flag the command ignores.\n */\nexport const refuseIfDevRunning = (\n root: string,\n action: string,\n options: { runtimeDir?: string; isolatedHint?: boolean } = {}\n): void => {\n const lock = readDevLock(resolveRuntimeDir(root, options.runtimeDir));\n if (lock) {\n const remedies = options.isolatedHint\n ? \"Reuse that server, stop it first, or re-run with --isolated to build/verify against .blume-verify without touching it.\"\n : \"Reuse that server or stop it first.\";\n logger.error(\n `A \\`blume dev\\` server is running${describeDevLock(lock)}; ${action} would corrupt its .blume runtime. ${remedies}`\n );\n process.exit(1);\n }\n};\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "1007843429DAE30664756E2164756E21",
9
+ "names": []
10
+ }