blume 1.4.3 → 1.5.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.
- package/CHANGELOG.md +17 -0
- package/dist/cli/index.js +1621 -576
- package/dist/cli/index.js.map +109 -104
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/config-input.d.ts +79 -27
- package/dist/types/core/config.d.ts +2 -1
- package/dist/types/core/data.d.ts +16 -1
- package/dist/types/core/diagnostics.d.ts +5 -1
- package/dist/types/core/i18n-ui.d.ts +12 -0
- package/dist/types/core/schema.d.ts +112 -15
- package/dist/types/core/sources/types.d.ts +3 -1
- package/dist/types/core/standard-schema.d.ts +7 -3
- package/dist/types/core/types.d.ts +43 -2
- package/dist/types/core/ui-packs/index.d.ts +9 -1
- package/dist/types/openapi/references.d.ts +6 -5
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/search.mdx +2 -0
- package/docs/configuration/seo.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/meta.mdx +2 -1
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +35 -1
- package/docs/content/versioning.mdx +106 -0
- package/docs/reference/cli.mdx +1 -0
- package/docs/reference/frontmatter.mdx +3 -0
- package/package.json +3 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +1 -1
- package/src/ai/agent-readability.ts +37 -10
- package/src/ai/ask-context.ts +5 -1
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +40 -16
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +28 -11
- package/src/ai/mcp/server.ts +183 -38
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +6 -2
- package/src/astro/generate.ts +54 -29
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +16 -9
- package/src/astro/templates.ts +152 -33
- package/src/audit/agent.ts +2 -2
- package/src/audit/checks/content.ts +26 -11
- package/src/audit/checks/dns-aid.ts +3 -0
- package/src/audit/checks/indexability.ts +24 -6
- package/src/audit/checks/llms.ts +9 -4
- package/src/audit/checks/network.ts +2 -0
- package/src/audit/checks/social.ts +18 -10
- package/src/audit/crawl.ts +37 -9
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +5 -2
- package/src/audit/snapshot.ts +2 -4
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/build.ts +15 -9
- package/src/cli/commands/dev.ts +2 -0
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eval.ts +7 -3
- package/src/cli/commands/init.ts +9 -9
- package/src/cli/commands/mcp-stdio.ts +3 -0
- package/src/cli/commands/translate.ts +14 -3
- package/src/cli/commands/version.ts +85 -0
- package/src/cli/dev-lock.ts +31 -10
- package/src/cli/eject-scripts.ts +17 -2
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +22 -15
- package/src/cli/internal-error.ts +1 -0
- package/src/components/content/auto-type-table.ts +3 -0
- package/src/components/content/diff.ts +9 -5
- package/src/components/content/github-info.ts +2 -0
- package/src/components/islands/ask-ai.tsx +33 -25
- package/src/components/islands/hooks.ts +5 -1
- package/src/components/islands/webmcp.ts +49 -12
- package/src/components/layout/Header.astro +25 -1
- package/src/components/layout/NavSelector.astro +11 -2
- package/src/components/layout/NavTree.astro +4 -2
- package/src/components/layout/RootLayout.astro +18 -0
- package/src/components/layout/Search.astro +77 -13
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/hydration-hint.ts +1 -1
- package/src/components/layout/nav-utils.ts +1 -4
- package/src/components/layout/overrides.ts +25 -12
- package/src/components/layout/search/algolia.ts +18 -5
- package/src/components/layout/search/endpoint.ts +3 -0
- package/src/components/layout/search/flexsearch.ts +23 -7
- package/src/components/layout/search/orama-cloud.ts +1 -1
- package/src/components/layout/search/orama.ts +4 -1
- package/src/components/layout/search/pagefind.ts +2 -0
- package/src/components/layout/search/types.ts +13 -1
- package/src/components/layout/search/typesense.ts +19 -3
- package/src/components/openapi/ApiOverview.astro +32 -6
- package/src/components/openapi/AsyncApiOperation.astro +237 -0
- package/src/components/openapi/Bindings.astro +89 -0
- package/src/components/openapi/MethodBadge.astro +3 -0
- package/src/components/openapi/Operation.astro +7 -2
- package/src/components/openapi/PanelTabs.astro +131 -0
- package/src/components/openapi/ParametersTable.astro +2 -0
- package/src/components/openapi/RequestPanel.astro +12 -119
- package/src/components/openapi/async-snippets.ts +174 -0
- package/src/components/openapi/async.ts +348 -0
- package/src/components/openapi/helpers.ts +52 -20
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +88 -27
- package/src/core/config.ts +20 -7
- package/src/core/content.ts +3 -1
- package/src/core/data.ts +16 -1
- package/src/core/define-components.ts +5 -0
- package/src/core/diagnostics.ts +46 -38
- package/src/core/frontmatter.ts +33 -7
- package/src/core/graph.ts +137 -53
- package/src/core/i18n-ui.ts +15 -0
- package/src/core/i18n.ts +16 -8
- package/src/core/load-module.ts +1 -0
- package/src/core/manifest.ts +92 -3
- package/src/core/meta.ts +44 -14
- package/src/core/nav-diagnostics.ts +3 -3
- package/src/core/navigation.ts +247 -67
- package/src/core/project-graph.ts +15 -3
- package/src/core/schema.ts +213 -67
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/github-releases.ts +39 -31
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +67 -20
- package/src/core/sources/notion.ts +49 -17
- package/src/core/sources/portable-text.ts +32 -11
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +4 -0
- package/src/core/sources/watch.ts +1 -1
- package/src/core/standard-schema.ts +9 -3
- package/src/core/text-width.ts +26 -0
- package/src/core/tsconfig-aliases.ts +9 -5
- package/src/core/types.ts +45 -2
- package/src/core/ui-packs/index.ts +9 -1
- package/src/core/version-cut.ts +301 -0
- package/src/core/version.ts +2 -0
- package/src/core/versions.ts +170 -0
- package/src/deploy/adapter-output.ts +5 -2
- package/src/deploy/cloudflare-negotiation.ts +25 -10
- package/src/deploy/sitemap.ts +33 -1
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/report.ts +4 -4
- package/src/eval/run.ts +2 -2
- package/src/eval/schema.ts +1 -1
- package/src/markdown/base-links.ts +6 -6
- package/src/markdown/directives.ts +7 -1
- package/src/markdown/heading-anchors.ts +17 -6
- package/src/markdown/index.ts +73 -24
- package/src/markdown/inline-code.ts +14 -2
- package/src/markdown/language-icon.ts +6 -2
- package/src/markdown/mdast.ts +18 -4
- package/src/markdown/package-commands.ts +6 -8
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +30 -11
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +126 -57
- package/src/openapi/parse.ts +97 -5
- package/src/openapi/references.ts +12 -10
- package/src/openapi/render-mdx.ts +73 -34
- package/src/openapi/scalar.ts +6 -8
- package/src/openapi/source.ts +98 -28
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +25 -5
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +66 -20
- package/src/search/popular.ts +10 -5
- package/src/search/providers.ts +2 -2
- package/src/search/sync/index.ts +2 -0
- package/src/search/sync/typesense.ts +4 -2
- package/src/seo/jsonld.ts +24 -6
- package/src/seo/x-handle.ts +8 -3
- package/src/theme/chrome-icons.ts +7 -2
- package/src/theme/fonts.ts +8 -4
- package/src/theme/icons.ts +4 -2
- package/src/theme/palette.ts +22 -14
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +9 -5
- package/src/translate/run.ts +10 -4
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
package/src/audit/crawl.ts
CHANGED
|
@@ -103,6 +103,35 @@ const sitemapParser = new XMLParser({
|
|
|
103
103
|
removeNSPrefix: true,
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* What fast-xml-parser produces for a parsed element: a string for text
|
|
108
|
+
* content (`parseTagValue: false`), an object of child elements, an array for
|
|
109
|
+
* a repeated element, or null for a self-closed one.
|
|
110
|
+
*/
|
|
111
|
+
type XmlValue =
|
|
112
|
+
| string
|
|
113
|
+
| number
|
|
114
|
+
| boolean
|
|
115
|
+
| null
|
|
116
|
+
| undefined
|
|
117
|
+
| XmlValue[]
|
|
118
|
+
| { [element: string]: XmlValue };
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* fast-xml-parser models an element with child elements as an object; a
|
|
122
|
+
* text-only or self-closed element parses to a string or null instead, which
|
|
123
|
+
* these guards reject the same way the crawler always has.
|
|
124
|
+
*/
|
|
125
|
+
const isUrlsetElement = (value: XmlValue): value is { url?: XmlValue } =>
|
|
126
|
+
typeof value === "object" && value !== null;
|
|
127
|
+
|
|
128
|
+
const isUrlEntry = (
|
|
129
|
+
value: XmlValue
|
|
130
|
+
): value is { lastmod?: XmlValue; loc?: XmlValue } =>
|
|
131
|
+
typeof value === "object" && value !== null;
|
|
132
|
+
|
|
133
|
+
const isText = (value: XmlValue): value is string => typeof value === "string";
|
|
134
|
+
|
|
106
135
|
/**
|
|
107
136
|
* Parse `sitemap.xml`. Deliberately shallow: the checks only need the `<loc>`
|
|
108
137
|
* list, each loc's `<lastmod>`, and whether the document is a urlset at all.
|
|
@@ -113,9 +142,9 @@ export const parseSitemap = (
|
|
|
113
142
|
bytes: number
|
|
114
143
|
): SitemapDoc => {
|
|
115
144
|
const doc: SitemapDoc = { bytes, file, lastmod: new Map(), urls: [] };
|
|
116
|
-
let parsed:
|
|
145
|
+
let parsed: { sitemapindex?: XmlValue; urlset?: XmlValue };
|
|
117
146
|
try {
|
|
118
|
-
parsed = sitemapParser.parse(xml)
|
|
147
|
+
parsed = sitemapParser.parse(xml);
|
|
119
148
|
} catch {
|
|
120
149
|
doc.error = "no <urlset> element";
|
|
121
150
|
return doc;
|
|
@@ -126,20 +155,19 @@ export const parseSitemap = (
|
|
|
126
155
|
: "no <urlset> element";
|
|
127
156
|
return doc;
|
|
128
157
|
}
|
|
129
|
-
const
|
|
130
|
-
const entries =
|
|
131
|
-
typeof urlset === "object" && urlset !== null ? [urlset.url].flat() : [];
|
|
158
|
+
const { urlset } = parsed;
|
|
159
|
+
const entries = isUrlsetElement(urlset) ? [urlset.url].flat() : [];
|
|
132
160
|
for (const entry of entries) {
|
|
133
|
-
if (
|
|
161
|
+
if (!isUrlEntry(entry)) {
|
|
134
162
|
continue;
|
|
135
163
|
}
|
|
136
|
-
const { loc, lastmod } = entry
|
|
137
|
-
const locText =
|
|
164
|
+
const { loc, lastmod } = entry;
|
|
165
|
+
const locText = isText(loc) ? loc.trim() : "";
|
|
138
166
|
if (!locText) {
|
|
139
167
|
continue;
|
|
140
168
|
}
|
|
141
169
|
doc.urls.push(locText);
|
|
142
|
-
if (
|
|
170
|
+
if (isText(lastmod) && lastmod.trim() !== "") {
|
|
143
171
|
doc.lastmod?.set(locText, lastmod.trim());
|
|
144
172
|
}
|
|
145
173
|
}
|
package/src/audit/report.ts
CHANGED
|
@@ -9,26 +9,26 @@ import type { CheckId } from "./catalog.ts";
|
|
|
9
9
|
import type { AuditResult } from "./run.ts";
|
|
10
10
|
import type { AuditCategory, AuditTier } from "./types.ts";
|
|
11
11
|
|
|
12
|
-
const SEVERITY_COLOR
|
|
12
|
+
const SEVERITY_COLOR = {
|
|
13
13
|
error: colors.red,
|
|
14
14
|
info: colors.blue,
|
|
15
15
|
warning: colors.yellow,
|
|
16
|
-
}
|
|
16
|
+
} satisfies Record<DiagnosticSeverity, ColorFunction>;
|
|
17
17
|
|
|
18
|
-
const GLYPH
|
|
18
|
+
const GLYPH = {
|
|
19
19
|
error: "✖",
|
|
20
20
|
info: "ℹ",
|
|
21
21
|
warning: "⚠",
|
|
22
|
-
}
|
|
22
|
+
} satisfies Record<DiagnosticSeverity, string>;
|
|
23
23
|
|
|
24
24
|
/** How many affected pages to list before collapsing the rest. */
|
|
25
25
|
const PREVIEW = 3;
|
|
26
26
|
|
|
27
|
-
/** The
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
/** The skippable tiers, each with the flag that runs it. */
|
|
28
|
+
const TIER_FLAGS: { tier: AuditTier; flag: string }[] = [
|
|
29
|
+
{ flag: "--external", tier: "external" },
|
|
30
|
+
{ flag: "--url <origin>", tier: "network" },
|
|
31
|
+
];
|
|
32
32
|
|
|
33
33
|
interface CheckRollup {
|
|
34
34
|
id: CheckId;
|
|
@@ -55,18 +55,21 @@ export const rollup = (diagnostics: Diagnostic[]): CheckRollup[] => {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const order
|
|
58
|
+
const order = {
|
|
59
59
|
error: 0,
|
|
60
60
|
info: 2,
|
|
61
61
|
warning: 1,
|
|
62
|
-
}
|
|
62
|
+
} satisfies Record<DiagnosticSeverity, number>;
|
|
63
63
|
const checks = [...groups.entries()].map(([id, findings]) => {
|
|
64
|
-
|
|
64
|
+
// SAFETY: audit findings are only ever created through `finding()`, whose
|
|
65
|
+
// codes are the check catalog's ids.
|
|
66
|
+
const checkId = id as CheckId;
|
|
67
|
+
const { category, severity, title } = checkMeta(checkId);
|
|
65
68
|
return {
|
|
66
69
|
category,
|
|
67
70
|
count: findings.length,
|
|
68
71
|
findings,
|
|
69
|
-
id:
|
|
72
|
+
id: checkId,
|
|
70
73
|
severity,
|
|
71
74
|
title,
|
|
72
75
|
};
|
|
@@ -95,12 +98,10 @@ export const rollup = (diagnostics: Diagnostic[]): CheckRollup[] => {
|
|
|
95
98
|
|
|
96
99
|
/** Categories that had no findings but were never run, and the flag that runs them. */
|
|
97
100
|
const skippedTiers = (tiers: Record<AuditTier, boolean>): string[] =>
|
|
98
|
-
|
|
99
|
-
.filter((
|
|
100
|
-
.
|
|
101
|
-
|
|
102
|
-
return ` ${colors.dim(`⊘ ${tier.padEnd(12)} skipped — pass ${TIER_FLAG[tier]} (${label} checks)`)}`;
|
|
103
|
-
});
|
|
101
|
+
TIER_FLAGS.filter(({ tier }) => !tiers[tier]).map(({ flag, tier }) => {
|
|
102
|
+
const label = CHECKS.filter((check) => check.tier === tier).length;
|
|
103
|
+
return ` ${colors.dim(`⊘ ${tier.padEnd(12)} skipped — pass ${flag} (${label} checks)`)}`;
|
|
104
|
+
});
|
|
104
105
|
|
|
105
106
|
/** How many checks actually ran, i.e. those whose tier was enabled. */
|
|
106
107
|
const activeChecks = (tiers: Record<AuditTier, boolean>): number =>
|
package/src/audit/run.ts
CHANGED
|
@@ -183,11 +183,11 @@ export const runAudit = async (options: AuditOptions): Promise<AuditResult> => {
|
|
|
183
183
|
thresholds: DEFAULT_THRESHOLDS,
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
-
const tiers
|
|
186
|
+
const tiers = {
|
|
187
187
|
external: Boolean(options.external),
|
|
188
188
|
network: origin !== null,
|
|
189
189
|
static: true,
|
|
190
|
-
}
|
|
190
|
+
} satisfies Record<AuditTier, boolean>;
|
|
191
191
|
|
|
192
192
|
const results = await Promise.all(
|
|
193
193
|
MODULES.filter((module) => tiers[module.tier]).map((module) =>
|
|
@@ -197,11 +197,14 @@ export const runAudit = async (options: AuditOptions): Promise<AuditResult> => {
|
|
|
197
197
|
|
|
198
198
|
let diagnostics = results.flat();
|
|
199
199
|
if (options.only?.length) {
|
|
200
|
+
// SAFETY: audit diagnostics are created through `finding()`, whose codes
|
|
201
|
+
// all come from the check catalog's `CheckId` set.
|
|
200
202
|
diagnostics = diagnostics.filter((d) =>
|
|
201
203
|
matches(d.code as CheckId, options.only ?? [])
|
|
202
204
|
);
|
|
203
205
|
}
|
|
204
206
|
if (options.skip?.length) {
|
|
207
|
+
// SAFETY: same invariant — every audit diagnostic code is a catalog `CheckId`.
|
|
205
208
|
diagnostics = diagnostics.filter(
|
|
206
209
|
(d) => !matches(d.code as CheckId, options.skip ?? [])
|
|
207
210
|
);
|
package/src/audit/snapshot.ts
CHANGED
|
@@ -84,9 +84,7 @@ const collectAssets = (
|
|
|
84
84
|
.filter((asset) => asset.src.length > 0);
|
|
85
85
|
|
|
86
86
|
/** Parse each JSON-LD block, keeping the parse failures rather than dropping them. */
|
|
87
|
-
const collectJsonLd = (
|
|
88
|
-
document: HtmlDocument
|
|
89
|
-
): { jsonld: unknown[]; jsonldErrors: string[] } => {
|
|
87
|
+
const collectJsonLd = (document: HtmlDocument) => {
|
|
90
88
|
const jsonld: unknown[] = [];
|
|
91
89
|
const jsonldErrors: string[] = [];
|
|
92
90
|
for (const script of document.querySelectorAll(
|
|
@@ -106,7 +104,7 @@ const prefixedMeta = (
|
|
|
106
104
|
document: HtmlDocument,
|
|
107
105
|
keyAttr: "property" | "name",
|
|
108
106
|
prefix: string
|
|
109
|
-
)
|
|
107
|
+
) => {
|
|
110
108
|
const found: Record<string, string> = {};
|
|
111
109
|
for (const element of document.querySelectorAll(
|
|
112
110
|
`meta[${keyAttr}^="${prefix}"]`
|
package/src/audit/types.ts
CHANGED
|
@@ -161,11 +161,32 @@ export interface LinkGraph {
|
|
|
161
161
|
/** Astro's reserved error routes. Never indexable, never crawlable — by design. */
|
|
162
162
|
export const ERROR_ROUTES: ReadonlySet<string> = new Set(["/404", "/500"]);
|
|
163
163
|
|
|
164
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* Tunable limits. Not yet configurable — CLI-only until the ids settle.
|
|
166
|
+
*
|
|
167
|
+
* The title and description limits are measured in **display columns**, where
|
|
168
|
+
* a fullwidth or wide character counts 2 and everything else 1 — not in
|
|
169
|
+
* characters. What a search engine truncates is the space the text takes up,
|
|
170
|
+
* and a character count only stands in for that where every character is one
|
|
171
|
+
* column wide, which is true of Latin text and of nothing else. Counted in
|
|
172
|
+
* characters one range cannot serve both scripts: it is at once too strict for
|
|
173
|
+
* a Japanese description (which says in ~60 characters what English needs ~120
|
|
174
|
+
* for, and so reads as "too short") and too loose for a Japanese title (60
|
|
175
|
+
* characters render as wide as 120 Latin ones, and truncate). ASCII and
|
|
176
|
+
* precomposed (NFC) Latin text score identically either way; a decomposed
|
|
177
|
+
* (NFD) combining mark counts 0 where a character count saw 1. East-Asian
|
|
178
|
+
* "ambiguous"-width characters (★ ※ ①) count 1 per `string-width`'s default,
|
|
179
|
+
* though CJK fonts render them fullwidth — resolving that would need the
|
|
180
|
+
* page's language, which the audit does not thread through yet.
|
|
181
|
+
*/
|
|
165
182
|
export interface AuditThresholds {
|
|
183
|
+
/** Display columns, not characters. */
|
|
166
184
|
titleMin: number;
|
|
185
|
+
/** Display columns, not characters. */
|
|
167
186
|
titleMax: number;
|
|
187
|
+
/** Display columns, not characters. */
|
|
168
188
|
descriptionMin: number;
|
|
189
|
+
/** Display columns, not characters. */
|
|
169
190
|
descriptionMax: number;
|
|
170
191
|
minWordCount: number;
|
|
171
192
|
maxHtmlBytes: number;
|
|
@@ -174,8 +195,9 @@ export interface AuditThresholds {
|
|
|
174
195
|
}
|
|
175
196
|
|
|
176
197
|
export const DEFAULT_THRESHOLDS: AuditThresholds = {
|
|
177
|
-
// Ahrefs' guidance: 110–160
|
|
178
|
-
//
|
|
198
|
+
// Ahrefs' guidance: 110–160. Under ~110 wastes the snippet space search
|
|
199
|
+
// results give you; over ~160 gets truncated. Stated there in characters of
|
|
200
|
+
// English, which is the same number of columns.
|
|
179
201
|
descriptionMax: 160,
|
|
180
202
|
descriptionMin: 110,
|
|
181
203
|
maxAssetBytes: 500 * 1024,
|
package/src/blume-modules.d.ts
CHANGED
|
@@ -17,7 +17,10 @@ declare module "blume:search-client" {
|
|
|
17
17
|
|
|
18
18
|
declare module "blume:ask" {
|
|
19
19
|
/** The generated Ask trigger (see `askComponentTemplate`); empty when Ask is off. */
|
|
20
|
-
|
|
20
|
+
// Mirrors the generated `.blume/src/env.d.ts` declaration: the trigger's type
|
|
21
|
+
// is the AskAI island the enabled template re-exports.
|
|
22
|
+
// oxlint-disable-next-line typescript/consistent-type-imports
|
|
23
|
+
const Ask: typeof import("./components/islands/AskAI.astro").default;
|
|
21
24
|
export default Ask;
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -34,6 +37,7 @@ declare module "blume:data" {
|
|
|
34
37
|
// `tsc` (where the Astro TS plugin isn't active). Not shipped in `dist/types`, so
|
|
35
38
|
// consumers keep Astro's real `.astro` types and get the true prop shapes.
|
|
36
39
|
declare module "*.astro" {
|
|
40
|
+
// oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type, anti-slop/no-unknown-returns -- a wildcard shim spans every `.astro` component, whose prop shapes and render output only Astro's own TS plugin can type
|
|
37
41
|
const component: (props: Record<string, unknown>) => unknown;
|
|
38
42
|
export default component;
|
|
39
43
|
}
|
|
@@ -53,6 +53,9 @@ const launchAgentCode = async (
|
|
|
53
53
|
try {
|
|
54
54
|
return await launchAgent(cli.bin, prompt);
|
|
55
55
|
} catch (error) {
|
|
56
|
+
// SAFETY: only the `code` tag is inspected; a spawn failure throws an
|
|
57
|
+
// ErrnoException, and any other thrown value fails the comparison and
|
|
58
|
+
// rethrows unchanged.
|
|
56
59
|
if ((error as NodeJS.ErrnoException)?.code !== "ENOENT") {
|
|
57
60
|
throw error;
|
|
58
61
|
}
|
|
@@ -123,14 +126,16 @@ export const auditCommand = defineCommand({
|
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
const root = process.cwd();
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
+
const gateInput = args["fail-on"] ?? (args.strict ? "warning" : "error");
|
|
130
|
+
const gate = SEVERITIES.find((severity) => severity === gateInput);
|
|
131
|
+
if (gate === undefined) {
|
|
129
132
|
logger.error(
|
|
130
|
-
`Invalid --fail-on "${
|
|
133
|
+
`Invalid --fail-on "${gateInput}" (use ${SEVERITIES.join(" | ")}).`
|
|
131
134
|
);
|
|
132
135
|
process.exit(1);
|
|
133
136
|
}
|
|
137
|
+
// SAFETY: AGENTS is a closed record keyed by AgentKind, so its keys are
|
|
138
|
+
// exactly the agent kinds.
|
|
134
139
|
const agents = (Object.keys(AGENTS) as AgentKind[]).filter(
|
|
135
140
|
(kind) => args[kind]
|
|
136
141
|
);
|
|
@@ -64,6 +64,9 @@ import { prepareProject } from "../prepare.ts";
|
|
|
64
64
|
|
|
65
65
|
const ADAPTERS = ["vercel", "node", "netlify", "cloudflare"] as const;
|
|
66
66
|
|
|
67
|
+
const isAdapter = (value: string): value is (typeof ADAPTERS)[number] =>
|
|
68
|
+
ADAPTERS.some((adapter) => adapter === value);
|
|
69
|
+
|
|
67
70
|
const BUDGET_JS = "budget-js";
|
|
68
71
|
const BUDGET_CSS = "budget-css";
|
|
69
72
|
|
|
@@ -295,14 +298,13 @@ const emitVercelNegotiation = async (
|
|
|
295
298
|
if (!existsSync(configPath)) {
|
|
296
299
|
return;
|
|
297
300
|
}
|
|
298
|
-
const overrides: Record<string, string> = {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
};
|
|
301
|
+
const overrides: Record<string, string> = {};
|
|
302
|
+
if (hasApiCatalog(config)) {
|
|
303
|
+
overrides[API_CATALOG_PATH.slice(1)] = API_CATALOG_TYPE;
|
|
304
|
+
}
|
|
305
|
+
if (config.ai.webBotAuth.keys.length > 0) {
|
|
306
|
+
overrides[SIGNATURES_DIRECTORY_PATH.slice(1)] = SIGNATURES_DIRECTORY_TYPE;
|
|
307
|
+
}
|
|
306
308
|
// The homepage rewrite serves `/index.md` from the static layer, so its
|
|
307
309
|
// `x-markdown-tokens` estimate has to ride the routing config; the runtime
|
|
308
310
|
// endpoint stamps it on dev/server-rendered responses itself.
|
|
@@ -754,7 +756,7 @@ export const buildCommand = defineCommand({
|
|
|
754
756
|
logger.error(`Invalid --output "${args.output}" (use static | server).`);
|
|
755
757
|
process.exit(1);
|
|
756
758
|
}
|
|
757
|
-
if (args.adapter && !
|
|
759
|
+
if (args.adapter && !isAdapter(args.adapter)) {
|
|
758
760
|
logger.error(
|
|
759
761
|
`Invalid --adapter "${args.adapter}" (use ${ADAPTERS.join(" | ")}).`
|
|
760
762
|
);
|
|
@@ -765,8 +767,12 @@ export const buildCommand = defineCommand({
|
|
|
765
767
|
const project = await prepareProject({
|
|
766
768
|
mode: "build",
|
|
767
769
|
overrides: {
|
|
770
|
+
// SAFETY: an invalid --adapter exited above; a set flag is an ADAPTERS
|
|
771
|
+
// member.
|
|
768
772
|
adapter: args.adapter as (typeof ADAPTERS)[number] | undefined,
|
|
769
773
|
base: args.base,
|
|
774
|
+
// SAFETY: an invalid --output exited above; a set flag is static or
|
|
775
|
+
// server.
|
|
770
776
|
output: args.output as "server" | "static" | undefined,
|
|
771
777
|
},
|
|
772
778
|
preview: args.preview,
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -185,6 +185,8 @@ export const devCommand = defineCommand({
|
|
|
185
185
|
reportDiagnostics(next.diagnostics, root);
|
|
186
186
|
showBlumeErrorOverlay(next.diagnostics);
|
|
187
187
|
} catch (error) {
|
|
188
|
+
// SAFETY: regeneration failures come from the generator and Astro's
|
|
189
|
+
// server API, which raise Error instances; only the message is shown.
|
|
188
190
|
logger.error(`Regeneration failed: ${(error as Error).message}`);
|
|
189
191
|
}
|
|
190
192
|
}, 80);
|
|
@@ -23,6 +23,8 @@ const FALLBACK_NODE_RANGE = ">=22.12.0";
|
|
|
23
23
|
* doctor can never drift from what the package actually declares. */
|
|
24
24
|
const supportedNodeRange = (): string => {
|
|
25
25
|
try {
|
|
26
|
+
// SAFETY: this parses blume's own package.json; the optional fields cover
|
|
27
|
+
// an `engines` block going missing, and a bad read falls to the catch.
|
|
26
28
|
const pkg = JSON.parse(
|
|
27
29
|
readFileSync(join(packageRoot(), "package.json"), "utf-8")
|
|
28
30
|
) as { engines?: { node?: string } };
|
package/src/cli/commands/eval.ts
CHANGED
|
@@ -52,6 +52,9 @@ const launchAgentCode = async (
|
|
|
52
52
|
try {
|
|
53
53
|
return await launchAgent(AGENTS[agent].bin, prompt);
|
|
54
54
|
} catch (error) {
|
|
55
|
+
// SAFETY: only the `code` tag is inspected; a spawn failure throws an
|
|
56
|
+
// ErrnoException, and any other thrown value fails the comparison and
|
|
57
|
+
// rethrows unchanged.
|
|
55
58
|
if ((error as NodeJS.ErrnoException)?.code !== "ENOENT") {
|
|
56
59
|
throw error;
|
|
57
60
|
}
|
|
@@ -76,9 +79,7 @@ interface EvalFlags {
|
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
/** Validate the flag surface, exiting with a message on the first offense. */
|
|
79
|
-
const parseFlags = (
|
|
80
|
-
args: EvalFlags
|
|
81
|
-
): { agent: AgentKind; threshold: number; timeoutS: number } => {
|
|
82
|
+
const parseFlags = (args: EvalFlags) => {
|
|
82
83
|
if (!isAgentKind(args.agent)) {
|
|
83
84
|
logger.error(`Invalid --agent "${args.agent}" (use claude | codex).`);
|
|
84
85
|
process.exit(1);
|
|
@@ -237,6 +238,9 @@ export const evalCommand = defineCommand({
|
|
|
237
238
|
logger.error(error.diagnostic.message);
|
|
238
239
|
process.exit(1);
|
|
239
240
|
}
|
|
241
|
+
// SAFETY: only the `code` tag is inspected; a spawn failure throws an
|
|
242
|
+
// ErrnoException, and any other thrown value fails the comparison and
|
|
243
|
+
// falls through to the internal-error report.
|
|
240
244
|
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
|
|
241
245
|
notInstalled(agent);
|
|
242
246
|
}
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -16,11 +16,7 @@ import {
|
|
|
16
16
|
TEMPLATES,
|
|
17
17
|
validateContentDir,
|
|
18
18
|
} from "../init/scaffold.ts";
|
|
19
|
-
import type {
|
|
20
|
-
InitAnswers,
|
|
21
|
-
PackageManager,
|
|
22
|
-
Template,
|
|
23
|
-
} from "../init/scaffold.ts";
|
|
19
|
+
import type { InitAnswers } from "../init/scaffold.ts";
|
|
24
20
|
import { logger } from "../log.ts";
|
|
25
21
|
|
|
26
22
|
/**
|
|
@@ -45,6 +41,8 @@ const ejectScaffold = async (
|
|
|
45
41
|
const steps = [...cd, commands.install, commands.dev];
|
|
46
42
|
logger.box(`Next steps:\n\n ${steps.join("\n ")}\n`);
|
|
47
43
|
} catch (error) {
|
|
44
|
+
// SAFETY: eject and the script rewrite throw Error instances; only the
|
|
45
|
+
// message is surfaced in the fallback hint.
|
|
48
46
|
logger.warn(
|
|
49
47
|
`Scaffolded, but eject needs the project's dependencies installed to load blume.config.ts: ${(error as Error).message}`
|
|
50
48
|
);
|
|
@@ -93,15 +91,17 @@ export const initCommand = defineCommand({
|
|
|
93
91
|
async run({ args }) {
|
|
94
92
|
const cwd = process.cwd();
|
|
95
93
|
|
|
96
|
-
const template =
|
|
97
|
-
if (template !== undefined &&
|
|
94
|
+
const template = TEMPLATES.find((candidate) => candidate === args.template);
|
|
95
|
+
if (args.template !== undefined && template === undefined) {
|
|
98
96
|
logger.error(
|
|
99
97
|
`Unknown template "${args.template}" (use ${TEMPLATES.join(" | ")}).`
|
|
100
98
|
);
|
|
101
99
|
process.exit(1);
|
|
102
100
|
}
|
|
103
|
-
const pm =
|
|
104
|
-
|
|
101
|
+
const pm = PACKAGE_MANAGERS.find(
|
|
102
|
+
(candidate) => candidate === args["package-manager"]
|
|
103
|
+
);
|
|
104
|
+
if (args["package-manager"] !== undefined && pm === undefined) {
|
|
105
105
|
logger.error(
|
|
106
106
|
`Unknown package manager "${args["package-manager"]}" (use ${PACKAGE_MANAGERS.join(" | ")}).`
|
|
107
107
|
);
|
|
@@ -23,6 +23,9 @@ export const mcpStdioCommand = defineCommand({
|
|
|
23
23
|
// must go to stderr or the MCP client chokes on the stray line.
|
|
24
24
|
let data: McpData;
|
|
25
25
|
try {
|
|
26
|
+
// SAFETY: the snapshot is written by `blume eval` itself as
|
|
27
|
+
// JSON.stringify of an McpData; a hand-mangled file fails the parse and
|
|
28
|
+
// lands in the catch below.
|
|
26
29
|
data = JSON.parse(await readFile(args.data, "utf-8")) as McpData;
|
|
27
30
|
} catch (error) {
|
|
28
31
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -47,10 +47,17 @@ interface TranslateFlags {
|
|
|
47
47
|
timeout?: string;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** The validated flag surface `parseFlags` hands the command body. */
|
|
51
|
+
interface ParsedTranslateFlags {
|
|
52
|
+
agent: AgentKind | undefined;
|
|
53
|
+
concurrency: number;
|
|
54
|
+
timeoutS: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
/** Validate the flag surface, exiting with a message on the first offense. */
|
|
51
|
-
const parseFlags = (
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
const parseFlags = (args: TranslateFlags): ParsedTranslateFlags => {
|
|
59
|
+
// SAFETY: Object.keys of the AGENTS table returns exactly its declared
|
|
60
|
+
// AgentKind keys.
|
|
54
61
|
const agents = (Object.keys(AGENTS) as AgentKind[]).filter(
|
|
55
62
|
(kind) => args[kind]
|
|
56
63
|
);
|
|
@@ -227,6 +234,8 @@ export const translateCommand = defineCommand({
|
|
|
227
234
|
return;
|
|
228
235
|
}
|
|
229
236
|
|
|
237
|
+
// SAFETY: parseFlags exits unless --check was passed or exactly one
|
|
238
|
+
// agent flag was; on this non-check path the agent is therefore set.
|
|
230
239
|
const kind = agent as AgentKind;
|
|
231
240
|
process.stderr.write(
|
|
232
241
|
`${translateHeaderLine(workList.items.length, workList.targetLocales.length, kind)}\n\n`
|
|
@@ -290,6 +299,8 @@ export const translateCommand = defineCommand({
|
|
|
290
299
|
logger.error(error.diagnostic.message);
|
|
291
300
|
process.exit(1);
|
|
292
301
|
}
|
|
302
|
+
// SAFETY: an ENOENT from spawning the agent CLI is an ErrnoException;
|
|
303
|
+
// any other thrown shape reads `code` as undefined and falls through.
|
|
293
304
|
if ((error as NodeJS.ErrnoException)?.code === "ENOENT" && agent) {
|
|
294
305
|
notInstalled(agent);
|
|
295
306
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
|
|
3
|
+
import { loadConfig } from "../../core/config.ts";
|
|
4
|
+
import { BlumeError } from "../../core/diagnostics.ts";
|
|
5
|
+
import { CutError, cutVersion } from "../../core/version-cut.ts";
|
|
6
|
+
import { reportInternalError } from "../internal-error.ts";
|
|
7
|
+
import { logger } from "../log.ts";
|
|
8
|
+
|
|
9
|
+
export const versionCommand = defineCommand({
|
|
10
|
+
args: {
|
|
11
|
+
force: {
|
|
12
|
+
description: "Overwrite an existing snapshot directory.",
|
|
13
|
+
type: "boolean",
|
|
14
|
+
},
|
|
15
|
+
id: {
|
|
16
|
+
description: 'Version id to cut (e.g. "v1.0").',
|
|
17
|
+
required: false,
|
|
18
|
+
type: "positional",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
meta: {
|
|
22
|
+
description: "Freeze the current docs as an archived version.",
|
|
23
|
+
name: "version",
|
|
24
|
+
},
|
|
25
|
+
async run({ args }) {
|
|
26
|
+
const root = process.cwd();
|
|
27
|
+
|
|
28
|
+
if (!args.id) {
|
|
29
|
+
const { config } = await loadConfig(root);
|
|
30
|
+
if (!config.versions) {
|
|
31
|
+
logger.info(
|
|
32
|
+
"Versioning is not configured. Cut the first version with `blume version <id>` (e.g. `blume version v1.0`)."
|
|
33
|
+
);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const { current, archived } = config.versions;
|
|
37
|
+
process.stdout.write(
|
|
38
|
+
` ${current.label} (current)${current.badge ? ` — ${current.badge}` : ""}\n`
|
|
39
|
+
);
|
|
40
|
+
for (const version of archived) {
|
|
41
|
+
process.stdout.write(
|
|
42
|
+
` ${version.label ?? version.id} — ${version.id}/\n`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const result = await cutVersion(root, args.id, { force: args.force });
|
|
50
|
+
logger.success(
|
|
51
|
+
`Snapshot ${result.dir} (${result.copied} file(s) copied)`
|
|
52
|
+
);
|
|
53
|
+
const totalRewrites = result.rewritten.reduce(
|
|
54
|
+
(sum, entry) => sum + entry.count,
|
|
55
|
+
0
|
|
56
|
+
);
|
|
57
|
+
if (totalRewrites > 0) {
|
|
58
|
+
logger.info(
|
|
59
|
+
`Rewrote root-absolute links in ${result.rewritten.length} page(s) (${totalRewrites} line(s)).`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (result.configUpdated) {
|
|
63
|
+
logger.success(
|
|
64
|
+
`Added "${args.id}" to versions.archived in blume.config.ts`
|
|
65
|
+
);
|
|
66
|
+
} else if (result.configSnippet) {
|
|
67
|
+
logger.info(result.configSnippet);
|
|
68
|
+
}
|
|
69
|
+
logger.info(
|
|
70
|
+
"Archived versions are frozen — future edits belong in the live tree. Restart `blume dev` to pick up the snapshot."
|
|
71
|
+
);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error instanceof CutError) {
|
|
74
|
+
logger.error(error.message);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
if (error instanceof BlumeError) {
|
|
78
|
+
logger.error(error.diagnostic.message);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
reportInternalError(error);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
});
|