blume 1.4.2 → 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 +71 -0
- package/dist/cli/index.js +2260 -1100
- package/dist/cli/index.js.map +123 -117
- package/dist/types/ai/component-markdown.d.ts +14 -4
- package/dist/types/core/base-path.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +87 -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 +116 -15
- package/dist/types/core/sources/types.d.ts +11 -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 +14 -7
- package/dist/types/seo/x-handle.d.ts +3 -2
- package/docs/advanced/api-reference.mdx +8 -6
- package/docs/configuration/ai.mdx +26 -8
- 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/sources.mdx +1 -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 +13 -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 +40 -12
- package/src/ai/api-catalog.ts +2 -2
- package/src/ai/ask-context.ts +49 -12
- package/src/ai/ask.ts +10 -1
- package/src/ai/component-markdown.ts +80 -43
- package/src/ai/llms.ts +42 -17
- package/src/ai/mcp/data.ts +48 -12
- package/src/ai/mcp/discovery.ts +52 -16
- package/src/ai/mcp/server.ts +280 -125
- package/src/ai/mcp/tools.ts +3 -3
- package/src/ai/skills.ts +32 -9
- package/src/ai/tar.ts +29 -70
- package/src/ai/visibility.ts +2 -2
- package/src/astro/component-slots.ts +2 -0
- package/src/astro/examples.ts +13 -5
- package/src/astro/generate.ts +113 -63
- package/src/astro/integration.ts +13 -2
- package/src/astro/islands.ts +23 -12
- package/src/astro/templates.ts +185 -41
- package/src/audit/agent.ts +16 -31
- 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 +78 -25
- package/src/audit/report.ts +20 -19
- package/src/audit/run.ts +15 -5
- package/src/audit/snapshot.ts +29 -6
- package/src/audit/types.ts +25 -3
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/commands/audit.ts +21 -21
- package/src/cli/commands/build.ts +30 -16
- package/src/cli/commands/dev.ts +15 -15
- package/src/cli/commands/doctor.ts +2 -0
- package/src/cli/commands/eject.ts +4 -4
- package/src/cli/commands/eval.ts +24 -30
- 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/env.ts +13 -30
- package/src/cli/index.ts +2 -0
- package/src/cli/init/questions.ts +1 -1
- package/src/cli/init/scaffold.ts +41 -13
- package/src/cli/internal-error.ts +1 -0
- package/src/cli/report-format.ts +22 -0
- package/src/components/content/AccordionItem.astro +2 -9
- package/src/components/content/ColorItem.astro +5 -13
- package/src/components/content/Component.astro +12 -8
- package/src/components/content/Frame.astro +2 -12
- package/src/components/content/Prompt.astro +12 -31
- package/src/components/content/Tab.astro +2 -9
- package/src/components/content/Tooltip.astro +1 -9
- package/src/components/content/Update.astro +2 -9
- 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/content/inline-markdown.ts +28 -0
- package/src/components/copy-feedback.ts +96 -0
- package/src/components/islands/ask-ai.tsx +111 -34
- 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/PageActions.astro +20 -32
- package/src/components/layout/PageLayout.astro +8 -28
- package/src/components/layout/RootLayout.astro +24 -48
- package/src/components/layout/Search.astro +133 -22
- package/src/components/layout/VersionBanner.astro +39 -0
- package/src/components/layout/analytics-client.ts +8 -5
- package/src/components/layout/drawer-inert.ts +31 -0
- 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 +8 -5
- package/src/components/layout/search/types.ts +45 -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/panel.ts +11 -8
- package/src/components/openapi/security.ts +102 -29
- package/src/components/openapi/snippets.ts +11 -11
- package/src/components/raf-throttle.ts +21 -0
- package/src/components/slug.ts +14 -0
- package/src/core/base-path.ts +18 -1
- package/src/core/component-overrides.ts +28 -23
- package/src/core/config-input.ts +96 -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 +74 -4
- 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/probe.ts +7 -19
- package/src/core/project-graph.ts +27 -4
- package/src/core/schema.ts +219 -67
- package/src/core/site-url.ts +27 -0
- package/src/core/sources/assets.ts +2 -0
- package/src/core/sources/cache.ts +16 -8
- package/src/core/sources/github-releases.ts +39 -11
- package/src/core/sources/mdx-remote.ts +4 -0
- package/src/core/sources/normalize.ts +93 -22
- package/src/core/sources/notion.ts +76 -22
- package/src/core/sources/portable-text.ts +48 -12
- package/src/core/sources/resolve.ts +1 -0
- package/src/core/sources/sanity.ts +68 -14
- package/src/core/sources/types.ts +17 -1
- 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 +40 -11
- package/src/deploy/robots.ts +2 -1
- package/src/deploy/rss.ts +2 -1
- package/src/deploy/sitemap.ts +89 -8
- package/src/deploy/vercel-negotiation.ts +11 -4
- package/src/eval/agents.ts +13 -10
- package/src/eval/report.ts +5 -18
- 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 +63 -58
- package/src/markdown/table-wrap.ts +4 -1
- package/src/markdown/twoslash.ts +2 -0
- package/src/og/card.ts +50 -33
- package/src/og/derive.ts +43 -27
- package/src/openapi/asyncapi.ts +366 -0
- package/src/openapi/model.ts +135 -66
- package/src/openapi/parse.ts +166 -33
- package/src/openapi/references.ts +47 -22
- package/src/openapi/render-mdx.ts +137 -59
- package/src/openapi/scalar.ts +8 -10
- package/src/openapi/source.ts +126 -29
- package/src/registry/eject.ts +7 -2
- package/src/search/documents.ts +103 -39
- package/src/search/facets.ts +7 -5
- package/src/search/orama-index.ts +117 -32
- 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 +27 -15
- package/src/translate/ledger.ts +4 -2
- package/src/translate/meta.ts +15 -6
- package/src/translate/report.ts +10 -19
- package/src/translate/run.ts +29 -38
- package/src/translate/validate.ts +52 -17
- package/src/translate/work-list.ts +0 -0
- package/src/cli/coalesce.ts +0 -43
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
platformRedirects,
|
|
55
55
|
} from "../../deploy/redirects.ts";
|
|
56
56
|
import { buildRobots } from "../../deploy/robots.ts";
|
|
57
|
-
import {
|
|
57
|
+
import { buildSitemapFiles } from "../../deploy/sitemap.ts";
|
|
58
58
|
import { injectNegotiationRoutes } from "../../deploy/vercel-negotiation.ts";
|
|
59
59
|
import { buildSearchIndex } from "../../search/build.ts";
|
|
60
60
|
import { syncSearchProvider } from "../../search/sync/index.ts";
|
|
@@ -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.
|
|
@@ -616,10 +618,18 @@ const publishBuildArtifacts = async (
|
|
|
616
618
|
}
|
|
617
619
|
|
|
618
620
|
// A user's own public/ file (copied into dist by Astro) always wins.
|
|
619
|
-
const
|
|
620
|
-
if (
|
|
621
|
-
await
|
|
622
|
-
|
|
621
|
+
const sitemapFiles = buildSitemapFiles(project);
|
|
622
|
+
if (sitemapFiles && !existsSync(join(distDir, "sitemap.xml"))) {
|
|
623
|
+
await Promise.all(
|
|
624
|
+
sitemapFiles.map((file) =>
|
|
625
|
+
writeFile(join(distDir, file.name), file.xml, "utf-8")
|
|
626
|
+
)
|
|
627
|
+
);
|
|
628
|
+
logger.success(
|
|
629
|
+
sitemapFiles.length === 1
|
|
630
|
+
? "Generated sitemap.xml"
|
|
631
|
+
: `Generated sitemap.xml (index of ${sitemapFiles.length - 1} sitemap files)`
|
|
632
|
+
);
|
|
623
633
|
}
|
|
624
634
|
|
|
625
635
|
const robots = buildRobots(project);
|
|
@@ -649,7 +659,7 @@ const publishBuildArtifacts = async (
|
|
|
649
659
|
|
|
650
660
|
const { config } = project;
|
|
651
661
|
const features = serverFeatures(config);
|
|
652
|
-
// `
|
|
662
|
+
// `buildSitemapFiles` returns null both when the sitemap is disabled and when no
|
|
653
663
|
// `site` is configured — only the latter deserves the remediation hint.
|
|
654
664
|
const sitemapNote = config.seo.sitemap
|
|
655
665
|
? "no (set deployment.site)"
|
|
@@ -661,7 +671,7 @@ const publishBuildArtifacts = async (
|
|
|
661
671
|
`Site ${config.deployment.site ?? "not set"}`,
|
|
662
672
|
`Search ${config.search.provider}`,
|
|
663
673
|
`Redirects ${config.redirects.length}`,
|
|
664
|
-
`Sitemap ${
|
|
674
|
+
`Sitemap ${sitemapFiles ? "yes" : sitemapNote}`,
|
|
665
675
|
`Robots ${robots ? "yes" : "no"}`,
|
|
666
676
|
`Agent JSON ${agentReadability ? "yes" : "no"}`,
|
|
667
677
|
`LLM files ${config.ai.llmsTxt.enabled ? "yes" : "no"}`,
|
|
@@ -746,7 +756,7 @@ export const buildCommand = defineCommand({
|
|
|
746
756
|
logger.error(`Invalid --output "${args.output}" (use static | server).`);
|
|
747
757
|
process.exit(1);
|
|
748
758
|
}
|
|
749
|
-
if (args.adapter && !
|
|
759
|
+
if (args.adapter && !isAdapter(args.adapter)) {
|
|
750
760
|
logger.error(
|
|
751
761
|
`Invalid --adapter "${args.adapter}" (use ${ADAPTERS.join(" | ")}).`
|
|
752
762
|
);
|
|
@@ -757,8 +767,12 @@ export const buildCommand = defineCommand({
|
|
|
757
767
|
const project = await prepareProject({
|
|
758
768
|
mode: "build",
|
|
759
769
|
overrides: {
|
|
770
|
+
// SAFETY: an invalid --adapter exited above; a set flag is an ADAPTERS
|
|
771
|
+
// member.
|
|
760
772
|
adapter: args.adapter as (typeof ADAPTERS)[number] | undefined,
|
|
761
773
|
base: args.base,
|
|
774
|
+
// SAFETY: an invalid --output exited above; a set flag is static or
|
|
775
|
+
// server.
|
|
762
776
|
output: args.output as "server" | "static" | undefined,
|
|
763
777
|
},
|
|
764
778
|
preview: args.preview,
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { dev } from "astro";
|
|
2
2
|
import { watch } from "chokidar";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
|
+
import { debounce } from "perfect-debounce";
|
|
4
5
|
|
|
5
6
|
import { generateRuntime } from "../../astro/generate.ts";
|
|
6
7
|
import { showBlumeErrorOverlay } from "../../astro/integration.ts";
|
|
7
8
|
import { scanProject } from "../../core/project-graph.ts";
|
|
8
9
|
import { resolveRuntimeDir } from "../../core/project.ts";
|
|
9
10
|
import { parsePort } from "../args.ts";
|
|
10
|
-
import { coalescedRunner } from "../coalesce.ts";
|
|
11
11
|
import {
|
|
12
12
|
acquireDevLock,
|
|
13
13
|
describeDevLock,
|
|
@@ -146,10 +146,16 @@ export const devCommand = defineCommand({
|
|
|
146
146
|
// watcher misses directory renames, so a renamed page 404s (`getEntry` reads
|
|
147
147
|
// a stale in-memory store) until the server is restarted. We restart it
|
|
148
148
|
// ourselves — stop, regenerate while down (no watcher races), then bring up
|
|
149
|
-
// a fresh container whose cold sync re-globs everything.
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
149
|
+
// a fresh container whose cold sync re-globs everything. perfect-debounce
|
|
150
|
+
// both debounces the watch burst (80ms) and single-flights the scan: a
|
|
151
|
+
// trigger during a run never starts a second run, only marks one trailing
|
|
152
|
+
// rerun after the current settles. Both halves are load-bearing — a plain
|
|
153
|
+
// debounce once let bursts stack overlapping scans until the heap was
|
|
154
|
+
// exhausted (observed as an OOM after minutes of looping). The contract is
|
|
155
|
+
// pinned by test/dev-debounce.test.ts. The task must not reject (the
|
|
156
|
+
// library re-invokes it from an unhandled .finally), so the body catches
|
|
157
|
+
// its own errors and always resolves.
|
|
158
|
+
const regenerate = debounce(async () => {
|
|
153
159
|
try {
|
|
154
160
|
const next = await scanProject(root, {
|
|
155
161
|
devServerUrl,
|
|
@@ -179,23 +185,17 @@ export const devCommand = defineCommand({
|
|
|
179
185
|
reportDiagnostics(next.diagnostics, root);
|
|
180
186
|
showBlumeErrorOverlay(next.diagnostics);
|
|
181
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.
|
|
182
190
|
logger.error(`Regeneration failed: ${(error as Error).message}`);
|
|
183
191
|
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
187
|
-
const regenerate = () => {
|
|
188
|
-
if (timer) {
|
|
189
|
-
clearTimeout(timer);
|
|
190
|
-
}
|
|
191
|
-
timer = setTimeout(runRegenerate, 80);
|
|
192
|
-
};
|
|
192
|
+
}, 80);
|
|
193
193
|
|
|
194
194
|
// The runtime prepared above baked the *requested* port into the site
|
|
195
195
|
// fallback; if Vite bumped it, regenerate so OG images, canonicals, and
|
|
196
196
|
// other site-gated URLs point at the port actually serving.
|
|
197
197
|
if (boundPort !== port) {
|
|
198
|
-
void
|
|
198
|
+
void regenerate();
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
// Content is watched per source (filesystem uses fs.watch; remote sources
|
|
@@ -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 } };
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
droppedArtifactNotices,
|
|
9
9
|
updatePackageScripts,
|
|
10
10
|
} from "../eject-scripts.ts";
|
|
11
|
-
import { commandsFor,
|
|
11
|
+
import { commandsFor, detectProjectPackageManager } from "../init/scaffold.ts";
|
|
12
12
|
import { logger } from "../log.ts";
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -76,9 +76,9 @@ export const ejectCommand = defineCommand({
|
|
|
76
76
|
process.stdout.write(` ${relative(root, file)}\n`);
|
|
77
77
|
}
|
|
78
78
|
reportDroppedArtifacts(notices);
|
|
79
|
-
// Print run commands matching the
|
|
80
|
-
//
|
|
81
|
-
const pm =
|
|
79
|
+
// Print run commands matching the project's package manager (lockfile
|
|
80
|
+
// detection, since eject runs inside an existing project).
|
|
81
|
+
const pm = await detectProjectPackageManager(root);
|
|
82
82
|
const { build, dev } = commandsFor(pm);
|
|
83
83
|
logger.box(
|
|
84
84
|
`Your project is now a standalone Astro app.\n\n ${dev}\n ${build}\n\nThe blume package remains importable.`
|
package/src/cli/commands/eval.ts
CHANGED
|
@@ -3,11 +3,7 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { join } from "pathe";
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
AGENTS,
|
|
8
|
-
launchAgent,
|
|
9
|
-
WINDOWS_COMMAND_NOT_FOUND,
|
|
10
|
-
} from "../../audit/agent.ts";
|
|
6
|
+
import { AGENTS, launchAgent } from "../../audit/agent.ts";
|
|
11
7
|
import type { AgentKind } from "../../audit/agent.ts";
|
|
12
8
|
import { BlumeError } from "../../core/diagnostics.ts";
|
|
13
9
|
import { scanProject } from "../../core/project-graph.ts";
|
|
@@ -37,32 +33,35 @@ const DEFAULT_TIMEOUT_S = 180;
|
|
|
37
33
|
const isAgentKind = (value: string): value is AgentKind => value in AGENTS;
|
|
38
34
|
|
|
39
35
|
/**
|
|
40
|
-
* Launch the interactive agent CLI,
|
|
41
|
-
*
|
|
42
|
-
*
|
|
36
|
+
* Launch the interactive agent CLI, turning a missing executable into the
|
|
37
|
+
* install hint. Only `ENOENT` means "not installed" — any other spawn failure
|
|
38
|
+
* (`EACCES`, `EMFILE`, …) must surface as itself.
|
|
43
39
|
*/
|
|
40
|
+
const notInstalled = (agent: AgentKind): never => {
|
|
41
|
+
const cli = AGENTS[agent];
|
|
42
|
+
logger.error(
|
|
43
|
+
`${cli.name} (\`${cli.bin}\`) was not found on PATH. Install it with \`${cli.install}\`.`
|
|
44
|
+
);
|
|
45
|
+
return process.exit(1);
|
|
46
|
+
};
|
|
47
|
+
|
|
44
48
|
const launchAgentCode = async (
|
|
45
|
-
|
|
49
|
+
agent: AgentKind,
|
|
46
50
|
prompt: string
|
|
47
51
|
): Promise<number> => {
|
|
48
52
|
try {
|
|
49
|
-
return await launchAgent(bin, prompt);
|
|
53
|
+
return await launchAgent(AGENTS[agent].bin, prompt);
|
|
50
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.
|
|
51
58
|
if ((error as NodeJS.ErrnoException)?.code !== "ENOENT") {
|
|
52
59
|
throw error;
|
|
53
60
|
}
|
|
54
|
-
return
|
|
61
|
+
return notInstalled(agent);
|
|
55
62
|
}
|
|
56
63
|
};
|
|
57
64
|
|
|
58
|
-
const notInstalled = (agent: AgentKind): never => {
|
|
59
|
-
const cli = AGENTS[agent];
|
|
60
|
-
logger.error(
|
|
61
|
-
`${cli.name} (\`${cli.bin}\`) was not found on PATH. Install it with \`${cli.install}\`.`
|
|
62
|
-
);
|
|
63
|
-
return process.exit(1);
|
|
64
|
-
};
|
|
65
|
-
|
|
66
65
|
/** The fraction of run (non-skipped) questions that passed. */
|
|
67
66
|
export const passFraction = (result: EvalResult): number => {
|
|
68
67
|
const ran = result.results.length - result.counts.skip;
|
|
@@ -80,9 +79,7 @@ interface EvalFlags {
|
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
/** Validate the flag surface, exiting with a message on the first offense. */
|
|
83
|
-
const parseFlags = (
|
|
84
|
-
args: EvalFlags
|
|
85
|
-
): { agent: AgentKind; threshold: number; timeoutS: number } => {
|
|
82
|
+
const parseFlags = (args: EvalFlags) => {
|
|
86
83
|
if (!isAgentKind(args.agent)) {
|
|
87
84
|
logger.error(`Invalid --agent "${args.agent}" (use claude | codex).`);
|
|
88
85
|
process.exit(1);
|
|
@@ -125,10 +122,7 @@ const runFixHandoff = async (
|
|
|
125
122
|
process.stderr.write(
|
|
126
123
|
` Handing ${count} failed question${count === 1 ? "" : "s"} to ${cli.name}…\n\n`
|
|
127
124
|
);
|
|
128
|
-
const code = await launchAgentCode(
|
|
129
|
-
if (code === WINDOWS_COMMAND_NOT_FOUND) {
|
|
130
|
-
notInstalled(agent);
|
|
131
|
-
}
|
|
125
|
+
const code = await launchAgentCode(agent, evalFixPrompt(report));
|
|
132
126
|
if (code !== 0) {
|
|
133
127
|
process.exit(code);
|
|
134
128
|
}
|
|
@@ -143,10 +137,7 @@ const runInit = async (agent: AgentKind, file: string): Promise<void> => {
|
|
|
143
137
|
);
|
|
144
138
|
process.exit(1);
|
|
145
139
|
}
|
|
146
|
-
const code = await launchAgentCode(
|
|
147
|
-
if (code === WINDOWS_COMMAND_NOT_FOUND) {
|
|
148
|
-
notInstalled(agent);
|
|
149
|
-
}
|
|
140
|
+
const code = await launchAgentCode(agent, initPrompt(file));
|
|
150
141
|
if (code !== 0) {
|
|
151
142
|
process.exit(code);
|
|
152
143
|
}
|
|
@@ -247,6 +238,9 @@ export const evalCommand = defineCommand({
|
|
|
247
238
|
logger.error(error.diagnostic.message);
|
|
248
239
|
process.exit(1);
|
|
249
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.
|
|
250
244
|
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
|
|
251
245
|
notInstalled(agent);
|
|
252
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
|
+
});
|
package/src/cli/dev-lock.ts
CHANGED
|
@@ -29,15 +29,32 @@ export interface DevLockInfo {
|
|
|
29
29
|
|
|
30
30
|
const lockPath = (outDir: string): string => join(outDir, "dev.lock");
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
/** What `JSON.parse` can yield for a lock file body. */
|
|
33
|
+
type LockFileValue =
|
|
34
|
+
| string
|
|
35
|
+
| number
|
|
36
|
+
| boolean
|
|
37
|
+
| null
|
|
38
|
+
| LockFileValue[]
|
|
39
|
+
| { [key: string]: LockFileValue };
|
|
40
|
+
|
|
41
|
+
const isValidPid = (pid: LockFileValue | undefined): pid is number =>
|
|
33
42
|
typeof pid === "number" && Number.isInteger(pid) && pid > 0;
|
|
34
43
|
|
|
44
|
+
const isPortNumber = (port: LockFileValue | undefined): port is number =>
|
|
45
|
+
typeof port === "number";
|
|
46
|
+
|
|
47
|
+
const isLockRecord = (
|
|
48
|
+
data: LockFileValue
|
|
49
|
+
): data is { pid?: LockFileValue; port?: LockFileValue } =>
|
|
50
|
+
typeof data === "object" && data !== null;
|
|
51
|
+
|
|
35
52
|
/**
|
|
36
53
|
* Parse a lock file body. Current locks are JSON (`{"pid":123,"port":3001}`);
|
|
37
54
|
* a bare integer (the pre-port format) still parses as a pid-only lock.
|
|
38
55
|
*/
|
|
39
56
|
const parseLock = (raw: string): DevLockInfo | null => {
|
|
40
|
-
let data:
|
|
57
|
+
let data: LockFileValue;
|
|
41
58
|
try {
|
|
42
59
|
data = JSON.parse(raw.trim());
|
|
43
60
|
} catch {
|
|
@@ -46,10 +63,10 @@ const parseLock = (raw: string): DevLockInfo | null => {
|
|
|
46
63
|
if (isValidPid(data)) {
|
|
47
64
|
return { pid: data };
|
|
48
65
|
}
|
|
49
|
-
if (
|
|
50
|
-
const { pid, port } = data
|
|
66
|
+
if (isLockRecord(data)) {
|
|
67
|
+
const { pid, port } = data;
|
|
51
68
|
if (isValidPid(pid)) {
|
|
52
|
-
return
|
|
69
|
+
return isPortNumber(port) ? { pid, port } : { pid };
|
|
53
70
|
}
|
|
54
71
|
}
|
|
55
72
|
return null;
|
|
@@ -63,6 +80,7 @@ const isProcessAlive = (pid: number): boolean => {
|
|
|
63
80
|
} catch (error) {
|
|
64
81
|
// EPERM means the process exists but belongs to another user — still
|
|
65
82
|
// live, so the lock must hold (only ESRCH proves it's gone).
|
|
83
|
+
// SAFETY: `process.kill` failures are errno exceptions.
|
|
66
84
|
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
67
85
|
}
|
|
68
86
|
};
|
|
@@ -84,11 +102,13 @@ export const readDevLock = (outDir: string): DevLockInfo | null => {
|
|
|
84
102
|
export const isDevLocked = (outDir: string): boolean =>
|
|
85
103
|
readDevLock(outDir) !== null;
|
|
86
104
|
|
|
87
|
-
const lockPayload = (port?: number): string =>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
105
|
+
const lockPayload = (port?: number): string => {
|
|
106
|
+
const info: DevLockInfo = { pid: process.pid };
|
|
107
|
+
if (port !== undefined) {
|
|
108
|
+
info.port = port;
|
|
109
|
+
}
|
|
110
|
+
return JSON.stringify(info);
|
|
111
|
+
};
|
|
92
112
|
|
|
93
113
|
const writeLock = (outDir: string, port?: number): void => {
|
|
94
114
|
writeFileSync(lockPath(outDir), lockPayload(port));
|
|
@@ -128,6 +148,7 @@ const tryClaimLock = (outDir: string, port?: number): boolean => {
|
|
|
128
148
|
writeFileSync(lockPath(outDir), lockPayload(port), { flag: "wx" });
|
|
129
149
|
return true;
|
|
130
150
|
} catch (error) {
|
|
151
|
+
// SAFETY: `writeFileSync` failures are errno exceptions.
|
|
131
152
|
if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
132
153
|
throw error;
|
|
133
154
|
}
|
package/src/cli/eject-scripts.ts
CHANGED
|
@@ -5,6 +5,21 @@ import { join } from "pathe";
|
|
|
5
5
|
import type { ResolvedConfig } from "../core/schema.ts";
|
|
6
6
|
import { searchProviderMeta } from "../search/providers.ts";
|
|
7
7
|
|
|
8
|
+
/** A JSON value, as `JSON.parse` of a manifest can return. */
|
|
9
|
+
type JsonValue =
|
|
10
|
+
| string
|
|
11
|
+
| number
|
|
12
|
+
| boolean
|
|
13
|
+
| null
|
|
14
|
+
| JsonValue[]
|
|
15
|
+
| { [key: string]: JsonValue };
|
|
16
|
+
|
|
17
|
+
/** The slice of package.json the rewrite touches; the rest rides along. */
|
|
18
|
+
interface PackageManifest {
|
|
19
|
+
[key: string]: JsonValue | undefined;
|
|
20
|
+
scripts?: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
/**
|
|
9
24
|
* The `blume build`-only artifacts this project's config actually produces, as
|
|
10
25
|
* notice lines for the eject command. After an eject the build script runs
|
|
@@ -55,13 +70,13 @@ export const droppedArtifactNotices = (config: ResolvedConfig): string[] => {
|
|
|
55
70
|
*/
|
|
56
71
|
export const updatePackageScripts = async (root: string): Promise<void> => {
|
|
57
72
|
const pkgPath = join(root, "package.json");
|
|
58
|
-
let pkg:
|
|
73
|
+
let pkg: PackageManifest;
|
|
59
74
|
try {
|
|
60
75
|
pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
61
76
|
} catch {
|
|
62
77
|
return;
|
|
63
78
|
}
|
|
64
|
-
const scripts =
|
|
79
|
+
const scripts = pkg.scripts ?? {};
|
|
65
80
|
pkg.scripts = {
|
|
66
81
|
...scripts,
|
|
67
82
|
build: "astro build",
|
package/src/cli/env.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { config } from "dotenv";
|
|
4
4
|
import { dirname, join, resolve } from "pathe";
|
|
5
5
|
|
|
6
6
|
// Blume's remote sources (GitHub Releases, mdx-remote, Sanity, Notion…) read
|
|
@@ -9,44 +9,27 @@ import { dirname, join, resolve } from "pathe";
|
|
|
9
9
|
// that gap: it cascades `.env`/`.env.local` from the working dir up to the repo
|
|
10
10
|
// root, so a monorepo can keep one `.env` at the root and every app picks it up.
|
|
11
11
|
|
|
12
|
-
/** Apply parsed vars without clobbering anything already in `process.env`. */
|
|
13
|
-
const applyEnv = (parsed: Record<string, string>): void => {
|
|
14
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
15
|
-
if (!(key in process.env)) {
|
|
16
|
-
process.env[key] = value;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const loadFile = (path: string): void => {
|
|
22
|
-
try {
|
|
23
|
-
if (existsSync(path)) {
|
|
24
|
-
// dotenv is the same parser Vite runs over these files at build time,
|
|
25
|
-
// so a value means the same thing to the pre-boot content scan and the
|
|
26
|
-
// built site — including multi-line double-quoted values (PEM keys),
|
|
27
|
-
// which a line-based parser silently truncates.
|
|
28
|
-
applyEnv(parse(readFileSync(path, "utf-8")));
|
|
29
|
-
}
|
|
30
|
-
} catch {
|
|
31
|
-
// Env files are best-effort; a read/parse failure must not abort a build.
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
|
|
35
12
|
/**
|
|
36
13
|
* Load `.env`/`.env.local`, cascading from `startDir` up to the repository root
|
|
37
|
-
* (the first ancestor containing a `.git`) or the filesystem root.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
14
|
+
* (the first ancestor containing a `.git`) or the filesystem root. The ordered
|
|
15
|
+
* path list is handed to dotenv, whose `config` is first-wins, never clobbers
|
|
16
|
+
* existing `process.env` values, and treats unreadable files as best-effort —
|
|
17
|
+
* so shell/CI overrides are never lost, `.env.local` layers over `.env`, and a
|
|
18
|
+
* bad file never aborts a build. dotenv is also the parser Vite runs over
|
|
19
|
+
* these files at build time, so a value means the same thing to the pre-boot
|
|
20
|
+
* content scan and the built site — including multi-line double-quoted values
|
|
21
|
+
* (PEM keys), which a line-based parser silently truncates.
|
|
40
22
|
*/
|
|
41
23
|
export const loadEnvFiles = (startDir: string): void => {
|
|
24
|
+
const paths: string[] = [];
|
|
42
25
|
let dir = resolve(startDir);
|
|
43
26
|
let done = false;
|
|
44
27
|
while (!done) {
|
|
45
|
-
|
|
46
|
-
loadFile(join(dir, ".env"));
|
|
28
|
+
paths.push(join(dir, ".env.local"), join(dir, ".env"));
|
|
47
29
|
const parent = dirname(dir);
|
|
48
30
|
// Stop at the repo root (nearest `.git`) or the filesystem root.
|
|
49
31
|
done = existsSync(join(dir, ".git")) || parent === dir;
|
|
50
32
|
dir = parent;
|
|
51
33
|
}
|
|
34
|
+
config({ path: paths, quiet: true });
|
|
52
35
|
};
|