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.
- package/CHANGELOG.md +41 -0
- package/bin/blume.mjs +3 -2
- package/dist/cli/chunk-0ewz4trd.js +679 -0
- package/dist/cli/chunk-0ewz4trd.js.map +15 -0
- package/dist/cli/chunk-27gtm2ym.js +69 -0
- package/dist/cli/chunk-27gtm2ym.js.map +11 -0
- package/dist/cli/chunk-2aj8ddew.js +72 -0
- package/dist/cli/chunk-2aj8ddew.js.map +10 -0
- package/dist/cli/chunk-3k0kzs6d.js +69 -0
- package/dist/cli/chunk-3k0kzs6d.js.map +11 -0
- package/dist/cli/chunk-3r94j3tc.js +221 -0
- package/dist/cli/chunk-3r94j3tc.js.map +10 -0
- package/dist/cli/chunk-4trphnvy.js +102 -0
- package/dist/cli/chunk-4trphnvy.js.map +11 -0
- package/dist/cli/chunk-4xyggvgf.js +21 -0
- package/dist/cli/chunk-4xyggvgf.js.map +10 -0
- package/dist/cli/chunk-5hs6gb7n.js +32 -0
- package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
- package/dist/cli/chunk-5yvt556e.js +185 -0
- package/dist/cli/chunk-5yvt556e.js.map +11 -0
- package/dist/cli/chunk-62qsssnh.js +3808 -0
- package/dist/cli/chunk-62qsssnh.js.map +36 -0
- package/dist/cli/chunk-6kzzpsx8.js +26 -0
- package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
- package/dist/cli/chunk-8gnpdsn1.js +952 -0
- package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
- package/dist/cli/chunk-9sh49q0h.js +30 -0
- package/dist/cli/chunk-9sh49q0h.js.map +10 -0
- package/dist/cli/chunk-aerwpe14.js +2370 -0
- package/dist/cli/chunk-aerwpe14.js.map +15 -0
- package/dist/cli/chunk-ag1zyr5x.js +176 -0
- package/dist/cli/chunk-ag1zyr5x.js.map +10 -0
- package/dist/cli/chunk-bawgnt8x.js +277 -0
- package/dist/cli/chunk-bawgnt8x.js.map +11 -0
- package/dist/cli/chunk-bcy492zc.js +16 -0
- package/dist/cli/chunk-bcy492zc.js.map +10 -0
- package/dist/cli/chunk-btfr9yvw.js +41 -0
- package/dist/cli/chunk-btfr9yvw.js.map +10 -0
- package/dist/cli/chunk-cbjnx4s8.js +73 -0
- package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
- package/dist/cli/chunk-cnvm6k3e.js +96 -0
- package/dist/cli/chunk-cnvm6k3e.js.map +10 -0
- package/dist/cli/chunk-etsqspj6.js +5170 -0
- package/dist/cli/chunk-etsqspj6.js.map +47 -0
- package/dist/cli/chunk-ev67ycx0.js +15 -0
- package/dist/cli/chunk-ev67ycx0.js.map +10 -0
- package/dist/cli/chunk-ey89bjj1.js +209 -0
- package/dist/cli/chunk-ey89bjj1.js.map +11 -0
- package/dist/cli/chunk-f75cqye8.js +76 -0
- package/dist/cli/chunk-f75cqye8.js.map +10 -0
- package/dist/cli/chunk-j00ezcg5.js +259 -0
- package/dist/cli/chunk-j00ezcg5.js.map +11 -0
- package/dist/cli/chunk-jtb45atp.js +467 -0
- package/dist/cli/chunk-jtb45atp.js.map +14 -0
- package/dist/cli/chunk-m3p3wahd.js +117 -0
- package/dist/cli/chunk-m3p3wahd.js.map +10 -0
- package/dist/cli/chunk-n0y172hf.js +387 -0
- package/dist/cli/chunk-n0y172hf.js.map +12 -0
- package/dist/cli/chunk-n4qjabmt.js +1062 -0
- package/dist/cli/chunk-n4qjabmt.js.map +25 -0
- package/dist/cli/chunk-nyqzjdhj.js +111 -0
- package/dist/cli/chunk-nyqzjdhj.js.map +11 -0
- package/dist/cli/chunk-pxj10x8y.js +35 -0
- package/dist/cli/chunk-pxj10x8y.js.map +10 -0
- package/dist/cli/chunk-s4jn7f1q.js +54 -0
- package/dist/cli/chunk-s4jn7f1q.js.map +10 -0
- package/dist/cli/chunk-s4k1pnvf.js +81 -0
- package/dist/cli/chunk-s4k1pnvf.js.map +10 -0
- package/dist/cli/chunk-s5e5jt53.js +227 -0
- package/dist/cli/chunk-s5e5jt53.js.map +11 -0
- package/dist/cli/chunk-sbdqrjbb.js +81 -0
- package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
- package/dist/cli/chunk-tc89yh2r.js +136 -0
- package/dist/cli/chunk-tc89yh2r.js.map +10 -0
- package/dist/cli/chunk-vt8fgygt.js +23 -0
- package/dist/cli/chunk-vt8fgygt.js.map +10 -0
- package/dist/cli/chunk-vv237fp3.js +1002 -0
- package/dist/cli/chunk-vv237fp3.js.map +13 -0
- package/dist/cli/chunk-vv3f8mb6.js +5314 -0
- package/dist/cli/chunk-vv3f8mb6.js.map +58 -0
- package/dist/cli/chunk-vxv4x1n8.js +17 -0
- package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
- package/dist/cli/chunk-wb067mv3.js +758 -0
- package/dist/cli/chunk-wb067mv3.js.map +13 -0
- package/dist/cli/chunk-wd27zjcz.js +60 -0
- package/dist/cli/chunk-wd27zjcz.js.map +10 -0
- package/dist/cli/chunk-wkq5tbtq.js +1141 -0
- package/dist/cli/chunk-wkq5tbtq.js.map +19 -0
- package/dist/cli/chunk-x1vrdjyk.js +1967 -0
- package/dist/cli/chunk-x1vrdjyk.js.map +34 -0
- package/dist/cli/chunk-x66c5yjn.js +23 -0
- package/dist/cli/chunk-x66c5yjn.js.map +10 -0
- package/dist/cli/index.js +55 -27587
- package/dist/cli/index.js.map +5 -243
- package/dist/types/ai/ask-context.d.ts +26 -0
- package/dist/types/core/code-fences.d.ts +11 -0
- package/dist/types/core/config-input.d.ts +10 -0
- package/dist/types/core/package-root.d.ts +1 -1
- package/dist/types/core/schema.d.ts +74 -1
- package/docs/02-deployment.mdx +1 -1
- package/docs/configuration/analytics.mdx +21 -2
- package/docs/configuration/ask-ai.mdx +1 -1
- package/docs/configuration/customization.mdx +2 -9
- package/docs/content/syntax.mdx +1 -1
- package/docs/reference/cli.mdx +1 -1
- package/package.json +16 -14
- package/src/ai/api/handlers.ts +4 -7
- package/src/ai/api/paths.ts +8 -0
- package/src/ai/api/spec.ts +2 -1
- package/src/ai/ask-context.ts +378 -22
- package/src/astro/generate.ts +25 -29
- package/src/astro/include-hmr.ts +10 -13
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +6 -1
- package/src/astro/integration.ts +269 -53
- package/src/astro/module-types.ts +74 -0
- package/src/astro/templates.ts +85 -97
- package/src/audit/image-size.ts +10 -8
- package/src/cli/command-meta.ts +77 -0
- package/src/cli/commands/add.ts +2 -4
- package/src/cli/commands/audit.ts +2 -4
- package/src/cli/commands/build.ts +42 -346
- package/src/cli/commands/check.ts +2 -4
- package/src/cli/commands/dev.ts +31 -42
- package/src/cli/commands/doctor.ts +2 -4
- package/src/cli/commands/eject.ts +3 -41
- package/src/cli/commands/eval.ts +2 -5
- package/src/cli/commands/init.ts +2 -4
- package/src/cli/commands/mcp-stdio.ts +2 -5
- package/src/cli/commands/preview.ts +3 -5
- package/src/cli/commands/sync.ts +2 -4
- package/src/cli/commands/translate.ts +2 -5
- package/src/cli/commands/validate.ts +2 -4
- package/src/cli/commands/version.ts +2 -4
- package/src/cli/eject-scripts.ts +0 -45
- package/src/cli/host-args.ts +16 -0
- package/src/cli/index.ts +84 -35
- package/src/cli/lazy-command.ts +47 -0
- package/src/components/content/GithubInfo.astro +4 -1
- package/src/components/content/mermaid-element.ts +8 -0
- package/src/components/layout/Analytics.astro +20 -1
- package/src/components/layout/PageLayout.astro +14 -3
- package/src/components/layout/ReferenceLayout.astro +15 -4
- package/src/components/layout/RootLayout.astro +15 -4
- package/src/components/layout/analytics-client.ts +2 -1
- package/src/components/layout/page-locale.ts +29 -0
- package/src/components/openapi/AsyncApiOperation.astro +5 -3
- package/src/components/openapi/Authorization.astro +4 -6
- package/src/components/openapi/Bindings.astro +2 -2
- package/src/components/openapi/Description.astro +109 -0
- package/src/components/openapi/GraphqlFieldsTable.astro +5 -7
- package/src/components/openapi/GraphqlOperation.astro +4 -3
- package/src/components/openapi/GraphqlType.astro +4 -6
- package/src/components/openapi/ParametersTable.astro +5 -7
- package/src/components/openapi/RequestBody.astro +2 -4
- package/src/components/openapi/Responses.astro +4 -3
- package/src/components/openapi/SchemaProperty.astro +11 -6
- package/src/components/openapi/description.ts +91 -0
- package/src/core/api-name.ts +18 -0
- package/src/core/code-fences.ts +48 -0
- package/src/core/config-input.ts +10 -0
- package/src/core/content-assets.ts +3 -7
- package/src/core/includes.ts +3 -7
- package/src/core/package-root.ts +1 -1
- package/src/core/schema.ts +27 -0
- package/src/core/sources/normalize.ts +2 -37
- package/src/core/sources/obsidian.ts +3 -2
- package/src/core/svg-dimensions.ts +97 -0
- package/src/core/version-cut.ts +2 -2
- package/src/deploy/artifacts.ts +370 -0
- package/src/deploy/cloudflare-negotiation.ts +97 -32
- package/src/deploy/function-bundle.ts +66 -20
- package/src/deploy/sitemap.ts +6 -0
- package/src/deploy/vercel-negotiation.ts +8 -30
- package/src/og/card.ts +6 -12
- package/src/openapi/render-mdx.ts +9 -5
- package/src/registry/eject.ts +0 -2
- package/src/theme/entry.ts +9 -2
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
4
|
import { build } from "astro";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
|
-
import {
|
|
6
|
+
import { join } from "pathe";
|
|
7
7
|
|
|
8
|
-
import { buildAgentReadability } from "../../ai/agent-readability.ts";
|
|
9
8
|
import {
|
|
10
9
|
API_CATALOG_PATH,
|
|
11
10
|
API_CATALOG_TYPE,
|
|
12
|
-
buildApiCatalog,
|
|
13
11
|
hasApiCatalog,
|
|
14
12
|
} from "../../ai/api-catalog.ts";
|
|
13
|
+
import { pageJsonPath } from "../../ai/api/paths.ts";
|
|
15
14
|
import { buildHomeLinkHeader } from "../../ai/link-headers.ts";
|
|
16
|
-
import { buildLlmsFiles } from "../../ai/llms.ts";
|
|
17
15
|
import {
|
|
18
16
|
agentMarkdown,
|
|
19
17
|
buildRawMarkdown,
|
|
@@ -21,16 +19,10 @@ import {
|
|
|
21
19
|
markdownTokenCount,
|
|
22
20
|
} from "../../ai/markdown.ts";
|
|
23
21
|
import {
|
|
24
|
-
AGENT_SKILLS_DIR,
|
|
25
|
-
buildSkillsIndex,
|
|
26
|
-
collectSkills,
|
|
27
|
-
} from "../../ai/skills.ts";
|
|
28
|
-
import type { SkillArtifact } from "../../ai/skills.ts";
|
|
29
|
-
import {
|
|
30
|
-
buildSignaturesDirectory,
|
|
31
22
|
SIGNATURES_DIRECTORY_PATH,
|
|
32
23
|
SIGNATURES_DIRECTORY_TYPE,
|
|
33
24
|
} from "../../ai/web-bot-auth.ts";
|
|
25
|
+
import { publishBuildProject } from "../../astro/integration.ts";
|
|
34
26
|
import { ensureGitignore } from "../../core/gitignore.ts";
|
|
35
27
|
import type { BlumeProject } from "../../core/project-graph.ts";
|
|
36
28
|
import type { ResolvedConfig } from "../../core/schema.ts";
|
|
@@ -39,7 +31,6 @@ import type { ProjectContext } from "../../core/types.ts";
|
|
|
39
31
|
import {
|
|
40
32
|
ADAPTER_IGNORE_DIRS,
|
|
41
33
|
deployStaticDir,
|
|
42
|
-
readsHeaderFiles,
|
|
43
34
|
servesClientSubdir,
|
|
44
35
|
surfaceAdapterOutput,
|
|
45
36
|
} from "../../deploy/adapter-output.ts";
|
|
@@ -52,18 +43,9 @@ import {
|
|
|
52
43
|
blumeDependencyNames,
|
|
53
44
|
functionBundleVerdict,
|
|
54
45
|
} from "../../deploy/function-bundle.ts";
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
buildNetlifyRedirects,
|
|
58
|
-
buildRedirectManifest,
|
|
59
|
-
buildVercelConfig,
|
|
60
|
-
platformRedirects,
|
|
61
|
-
} from "../../deploy/redirects.ts";
|
|
62
|
-
import { buildRobots } from "../../deploy/robots.ts";
|
|
63
|
-
import { buildSitemapFiles } from "../../deploy/sitemap.ts";
|
|
46
|
+
import { platformRedirects } from "../../deploy/redirects.ts";
|
|
64
47
|
import { injectNegotiationRoutes } from "../../deploy/vercel-negotiation.ts";
|
|
65
|
-
import {
|
|
66
|
-
import { syncSearchProvider } from "../../search/sync/index.ts";
|
|
48
|
+
import { commandMeta } from "../command-meta.ts";
|
|
67
49
|
import { refuseIfDevRunning } from "../dev-lock.ts";
|
|
68
50
|
import { logger } from "../log.ts";
|
|
69
51
|
import { prepareProject } from "../prepare.ts";
|
|
@@ -102,208 +84,6 @@ const validateBudgetFlags = (args: BudgetArgs): void => {
|
|
|
102
84
|
}
|
|
103
85
|
};
|
|
104
86
|
|
|
105
|
-
/**
|
|
106
|
-
* Emit platform redirect files for a static build (adapters wire redirects
|
|
107
|
-
* natively). Always writes the manifest; writes `_redirects`/`vercel.json` only
|
|
108
|
-
* when the user hasn't shipped one via public/. Note that Vercel's
|
|
109
|
-
* git-integration builds read `vercel.json` from the repository root only —
|
|
110
|
-
* the copy emitted here takes effect when the dist folder itself is deployed
|
|
111
|
-
* directly via the Vercel CLI.
|
|
112
|
-
*/
|
|
113
|
-
const emitRedirectFiles = async (
|
|
114
|
-
config: ResolvedConfig,
|
|
115
|
-
distDir: string
|
|
116
|
-
): Promise<void> => {
|
|
117
|
-
const redirects = platformRedirects(config);
|
|
118
|
-
if (redirects.length === 0 || config.deployment.output !== "static") {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
await writeFile(
|
|
122
|
-
join(distDir, "blume-redirects.json"),
|
|
123
|
-
buildRedirectManifest(redirects),
|
|
124
|
-
"utf-8"
|
|
125
|
-
);
|
|
126
|
-
const platformFiles = [
|
|
127
|
-
{ content: buildNetlifyRedirects(redirects), name: "_redirects" },
|
|
128
|
-
{ content: buildVercelConfig(redirects), name: "vercel.json" },
|
|
129
|
-
];
|
|
130
|
-
await Promise.all(
|
|
131
|
-
platformFiles.map((file) =>
|
|
132
|
-
existsSync(join(distDir, file.name))
|
|
133
|
-
? Promise.resolve()
|
|
134
|
-
: writeFile(join(distDir, file.name), file.content, "utf-8")
|
|
135
|
-
)
|
|
136
|
-
);
|
|
137
|
-
logger.success(`Emitted redirect files for ${redirects.length} redirect(s)`);
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Emit a `_headers` file so Netlify / Cloudflare serve the raw AI-ready
|
|
142
|
-
* endpoints (`*.md`, `*.mdx`, `*.txt`) with an explicit `charset=utf-8`. Without
|
|
143
|
-
* it those hosts send `text/markdown` / `text/plain` with no charset and
|
|
144
|
-
* browsers fall back to Windows-1252, garbling any non-ASCII docs (#82).
|
|
145
|
-
*
|
|
146
|
-
* The same file carries the rest of the agent-discovery surface that only a
|
|
147
|
-
* response header can express: the homepage `Link` header (RFC 8288, see
|
|
148
|
-
* `ai/link-headers.ts`), and the registered media types for the extensionless
|
|
149
|
-
* well-known files — `application/linkset+json` for the API catalog, the
|
|
150
|
-
* signatures directory, and the Agent Skills archives. A static host serves
|
|
151
|
-
* those as `octet-stream` or nothing at all without a rule.
|
|
152
|
-
*
|
|
153
|
-
* A `_headers` shipped in `public/` wins, exactly like `_redirects` — the opt-out
|
|
154
|
-
* is checked at its source rather than in `dist`, because on Cloudflare the file
|
|
155
|
-
* in `dist` is not necessarily the user's: `@astrojs/cloudflare` writes its own
|
|
156
|
-
* `_headers` (an immutable `Cache-Control` rule for `/_astro/*`) during the
|
|
157
|
-
* build, before this runs. Testing `dist` therefore read an adapter-generated
|
|
158
|
-
* file as a user opt-out and skipped silently. When both exist, the adapter's
|
|
159
|
-
* rules are preserved and ours are appended.
|
|
160
|
-
*
|
|
161
|
-
* Gated on {@link readsHeaderFiles}, not on `output === "static"`. A **Cloudflare
|
|
162
|
-
* server** build serves `dist/client` through the Worker's ASSETS binding, and
|
|
163
|
-
* Workers static assets honor `_headers` from that directory — so the file
|
|
164
|
-
* applies there too, and skipping it left every Cloudflare server build with no
|
|
165
|
-
* `Link` header and no media type on its own discovery files. The charset half
|
|
166
|
-
* of this file *is* redundant on a server build, because the runtime endpoint
|
|
167
|
-
* sets Content-Type on the Response itself; the `Link` and well-known halves are
|
|
168
|
-
* not, and one conclusion about the first was applied to all three.
|
|
169
|
-
*
|
|
170
|
-
* Exported for the test suite, which exercises it in a subprocess like the
|
|
171
|
-
* other command helpers.
|
|
172
|
-
*/
|
|
173
|
-
export const emitHeaderFiles = async (
|
|
174
|
-
project: BlumeProject,
|
|
175
|
-
distDir: string
|
|
176
|
-
): Promise<void> => {
|
|
177
|
-
const { config } = project;
|
|
178
|
-
if (
|
|
179
|
-
!readsHeaderFiles(config.deployment) ||
|
|
180
|
-
existsSync(join(project.context.root, "public", "_headers"))
|
|
181
|
-
) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
const ours = buildNetlifyHeaders(
|
|
185
|
-
config,
|
|
186
|
-
buildHomeLinkHeader(config, markdownRoutePaths(project))
|
|
187
|
-
);
|
|
188
|
-
// An adapter may have written its own rules here already (Cloudflare adds an
|
|
189
|
-
// immutable Cache-Control for /_astro/*). Keep them and append ours: both
|
|
190
|
-
// sets are wanted, and `_headers` has no merge semantics beyond order.
|
|
191
|
-
const target = join(distDir, "_headers");
|
|
192
|
-
const existing = existsSync(target) ? await readFile(target, "utf-8") : "";
|
|
193
|
-
await writeFile(
|
|
194
|
-
target,
|
|
195
|
-
existing ? `${existing.trimEnd()}\n${ours}` : ours,
|
|
196
|
-
"utf-8"
|
|
197
|
-
);
|
|
198
|
-
logger.success(
|
|
199
|
-
"Emitted _headers (UTF-8 Content-Type + homepage Link header)"
|
|
200
|
-
);
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Collect the Agent Skills `ai.skills` publishes, once per build, so both the
|
|
205
|
-
* skills surface and llms.txt (which lists them) read the same set. Empty
|
|
206
|
-
* when the feature is off, the directory is missing, nothing in it is
|
|
207
|
-
* publishable (each with a warning), or a user-shipped
|
|
208
|
-
* `public/.well-known/agent-skills/index.json` already owns the surface.
|
|
209
|
-
*/
|
|
210
|
-
const collectConfiguredSkills = async (
|
|
211
|
-
project: BlumeProject,
|
|
212
|
-
distDir: string
|
|
213
|
-
): Promise<SkillArtifact[]> => {
|
|
214
|
-
const configured = project.config.ai.skills;
|
|
215
|
-
if (!configured) {
|
|
216
|
-
return [];
|
|
217
|
-
}
|
|
218
|
-
const dir = resolve(project.context.root, configured);
|
|
219
|
-
if (!existsSync(dir)) {
|
|
220
|
-
logger.warn(
|
|
221
|
-
`ai.skills points at "${configured}" (${dir}), which does not exist; no skills published.`
|
|
222
|
-
);
|
|
223
|
-
return [];
|
|
224
|
-
}
|
|
225
|
-
if (existsSync(join(distDir, AGENT_SKILLS_DIR.slice(1), "index.json"))) {
|
|
226
|
-
return [];
|
|
227
|
-
}
|
|
228
|
-
const { skills, warnings } = await collectSkills(dir);
|
|
229
|
-
for (const warning of warnings) {
|
|
230
|
-
logger.warn(warning);
|
|
231
|
-
}
|
|
232
|
-
if (skills.length === 0) {
|
|
233
|
-
logger.warn(`ai.skills: no publishable skills found in "${configured}".`);
|
|
234
|
-
}
|
|
235
|
-
return skills;
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Publish the collected Agent Skills: copy each skill artifact under
|
|
240
|
-
* `.well-known/agent-skills/` and emit the discovery index. A user-shipped
|
|
241
|
-
* `public/.well-known/agent-skills/index.json` takes over the whole surface
|
|
242
|
-
* (the collector returns nothing then), matching every other generated
|
|
243
|
-
* artifact.
|
|
244
|
-
*/
|
|
245
|
-
const emitAgentSkills = async (
|
|
246
|
-
project: BlumeProject,
|
|
247
|
-
distDir: string,
|
|
248
|
-
skills: readonly SkillArtifact[]
|
|
249
|
-
): Promise<void> => {
|
|
250
|
-
if (skills.length === 0) {
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
const outDir = join(distDir, AGENT_SKILLS_DIR.slice(1));
|
|
254
|
-
await Promise.all(
|
|
255
|
-
skills.map(async (skill) => {
|
|
256
|
-
const target = join(outDir, skill.path);
|
|
257
|
-
await mkdir(dirname(target), { recursive: true });
|
|
258
|
-
await writeFile(target, skill.content);
|
|
259
|
-
})
|
|
260
|
-
);
|
|
261
|
-
await writeFile(
|
|
262
|
-
join(outDir, "index.json"),
|
|
263
|
-
buildSkillsIndex(skills, project.config),
|
|
264
|
-
"utf-8"
|
|
265
|
-
);
|
|
266
|
-
logger.success(
|
|
267
|
-
`Published ${skills.length} agent skill(s) (.well-known/agent-skills/index.json)`
|
|
268
|
-
);
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Emit the generated `.well-known` discovery files — the RFC 9727 API catalog
|
|
273
|
-
* and the Web Bot Auth signature directory — each skipped when the feature is
|
|
274
|
-
* off or when the user ships their own copy via public/ (already in dist by
|
|
275
|
-
* the time this runs).
|
|
276
|
-
*/
|
|
277
|
-
const emitWellKnownFiles = async (
|
|
278
|
-
config: ResolvedConfig,
|
|
279
|
-
distDir: string
|
|
280
|
-
): Promise<void> => {
|
|
281
|
-
const files = [
|
|
282
|
-
{
|
|
283
|
-
content: buildSignaturesDirectory(config),
|
|
284
|
-
label: "Web Bot Auth",
|
|
285
|
-
path: SIGNATURES_DIRECTORY_PATH,
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
content: buildApiCatalog(config),
|
|
289
|
-
label: "RFC 9727",
|
|
290
|
-
path: API_CATALOG_PATH,
|
|
291
|
-
},
|
|
292
|
-
];
|
|
293
|
-
for (const file of files) {
|
|
294
|
-
const target = join(distDir, file.path.slice(1));
|
|
295
|
-
if (!file.content || existsSync(target)) {
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
// Sequential by nature: both files share the .well-known dir creation.
|
|
299
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
300
|
-
await mkdir(join(distDir, ".well-known"), { recursive: true });
|
|
301
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
302
|
-
await writeFile(target, file.content, "utf-8");
|
|
303
|
-
logger.success(`Generated ${file.path.slice(1)} (${file.label})`);
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
|
|
307
87
|
/**
|
|
308
88
|
* Splice `Accept: text/markdown` negotiation routes into the Vercel adapter's
|
|
309
89
|
* Build Output config, so a content-page request that prefers Markdown gets the
|
|
@@ -436,6 +216,15 @@ const emitCloudflareNegotiation = async (
|
|
|
436
216
|
contentRoutePaths: project.manifest.routes.map((route) => route.path),
|
|
437
217
|
homeLinkHeader: buildHomeLinkHeader(config, routePaths),
|
|
438
218
|
homeTokens: home ? markdownTokenCount(agentMarkdown(home)) : undefined,
|
|
219
|
+
// Exactly the per-page JSON documents the API emits (see `pageParams`):
|
|
220
|
+
// the non-hidden routes with agent Markdown, when the API is on.
|
|
221
|
+
pageJsonPaths: config.ai.api
|
|
222
|
+
? project.manifest.routes
|
|
223
|
+
.filter(
|
|
224
|
+
(route) => !route.hidden && rawMarkdown[route.path] !== undefined
|
|
225
|
+
)
|
|
226
|
+
.map((route) => pageJsonPath(route.path))
|
|
227
|
+
: [],
|
|
439
228
|
// The wrapper Worker matches full served URLs, so the redirects are
|
|
440
229
|
// based the same way the platform files are — it answers any the
|
|
441
230
|
// worker-first rules claim, where `_redirects` is never consulted and
|
|
@@ -616,118 +405,22 @@ export const isolatedStaticDir = (
|
|
|
616
405
|
};
|
|
617
406
|
|
|
618
407
|
/**
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
408
|
+
* Print the build summary box and run the optional bundle report / budget
|
|
409
|
+
* gate against the served static dir. The deploy artifacts themselves
|
|
410
|
+
* (search index, llms.txt, sitemap, robots, redirect and header files, …)
|
|
411
|
+
* were written by the integration's `astro:build:done` hook during
|
|
412
|
+
* `build()` — see `deploy/artifacts.ts`. Exits non-zero if a budget is
|
|
413
|
+
* exceeded.
|
|
623
414
|
*/
|
|
624
|
-
const
|
|
625
|
-
project: BlumeProject,
|
|
626
|
-
distDir: string,
|
|
627
|
-
skills: readonly SkillArtifact[]
|
|
628
|
-
): Promise<void> => {
|
|
629
|
-
const indexPath = join(distDir, "llms.txt");
|
|
630
|
-
const fullPath = join(distDir, "llms-full.txt");
|
|
631
|
-
const writeIndex = !existsSync(indexPath);
|
|
632
|
-
const writeFull = !existsSync(fullPath);
|
|
633
|
-
if (!(writeIndex || writeFull)) {
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
const { index, full } = await buildLlmsFiles(project, { skills });
|
|
637
|
-
const writes: Promise<void>[] = [];
|
|
638
|
-
if (writeIndex) {
|
|
639
|
-
writes.push(writeFile(indexPath, index, "utf-8"));
|
|
640
|
-
}
|
|
641
|
-
if (writeFull) {
|
|
642
|
-
writes.push(writeFile(fullPath, full, "utf-8"));
|
|
643
|
-
}
|
|
644
|
-
await Promise.all(writes);
|
|
645
|
-
logger.success(
|
|
646
|
-
`Generated ${[
|
|
647
|
-
writeIndex ? "llms.txt" : null,
|
|
648
|
-
writeFull ? "llms-full.txt" : null,
|
|
649
|
-
]
|
|
650
|
-
.filter(Boolean)
|
|
651
|
-
.join(" and ")}`
|
|
652
|
-
);
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* Run every deploy post-step of a real (non-isolated) build: the search index +
|
|
657
|
-
* hosted-provider sync, llms.txt, sitemap/robots, redirect files, the summary
|
|
658
|
-
* box, and the optional bundle report / budget gate. Exits non-zero if a budget
|
|
659
|
-
* is exceeded. Isolated verify builds skip all of this except the bundle
|
|
660
|
-
* report / budget gate, which they run against their own output.
|
|
661
|
-
*/
|
|
662
|
-
const publishBuildArtifacts = async (
|
|
415
|
+
const reportBuild = async (
|
|
663
416
|
project: BlumeProject,
|
|
664
417
|
distDir: string,
|
|
665
418
|
args: { analyze?: boolean } & BudgetArgs
|
|
666
419
|
): Promise<void> => {
|
|
667
|
-
if (project.config.search.provider === "pagefind") {
|
|
668
|
-
logger.start("Building search index");
|
|
669
|
-
const indexed = await buildSearchIndex(distDir);
|
|
670
|
-
logger.success(`Indexed ${indexed} page(s) for search`);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Upload the index to a hosted provider (Algolia, Orama Cloud, Typesense).
|
|
674
|
-
// Skipped with a warning when its admin key isn't configured.
|
|
675
|
-
await syncSearchProvider(project, {
|
|
676
|
-
start: (message) => logger.start(message),
|
|
677
|
-
success: (message) => logger.success(message),
|
|
678
|
-
warn: (message) => logger.warn(message),
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
// Collected once: llms.txt lists the skills the build publishes below.
|
|
682
|
-
const skills = await collectConfiguredSkills(project, distDir);
|
|
683
|
-
if (project.config.ai.llmsTxt.enabled) {
|
|
684
|
-
await publishLlmsFiles(project, distDir, skills);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// A user's own public/ file (copied into dist by Astro) always wins.
|
|
688
|
-
const sitemapFiles = buildSitemapFiles(project);
|
|
689
|
-
if (sitemapFiles && !existsSync(join(distDir, "sitemap.xml"))) {
|
|
690
|
-
await Promise.all(
|
|
691
|
-
sitemapFiles.map((file) =>
|
|
692
|
-
writeFile(join(distDir, file.name), file.xml, "utf-8")
|
|
693
|
-
)
|
|
694
|
-
);
|
|
695
|
-
logger.success(
|
|
696
|
-
sitemapFiles.length === 1
|
|
697
|
-
? "Generated sitemap.xml"
|
|
698
|
-
: `Generated sitemap.xml (index of ${sitemapFiles.length - 1} sitemap files)`
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const robots = buildRobots(project);
|
|
703
|
-
if (robots && !existsSync(join(distDir, "robots.txt"))) {
|
|
704
|
-
await writeFile(join(distDir, "robots.txt"), robots, "utf-8");
|
|
705
|
-
logger.success("Generated robots.txt");
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
const agentReadability = buildAgentReadability(project);
|
|
709
|
-
if (
|
|
710
|
-
agentReadability &&
|
|
711
|
-
!existsSync(join(distDir, "agent-readability.json"))
|
|
712
|
-
) {
|
|
713
|
-
await writeFile(
|
|
714
|
-
join(distDir, "agent-readability.json"),
|
|
715
|
-
`${JSON.stringify(agentReadability, null, 2)}\n`,
|
|
716
|
-
"utf-8"
|
|
717
|
-
);
|
|
718
|
-
logger.success("Generated agent-readability.json");
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
await emitWellKnownFiles(project.config, distDir);
|
|
722
|
-
await emitAgentSkills(project, distDir, skills);
|
|
723
|
-
|
|
724
|
-
await emitRedirectFiles(project.config, distDir);
|
|
725
|
-
await emitHeaderFiles(project, distDir);
|
|
726
|
-
|
|
727
420
|
const { config } = project;
|
|
728
421
|
const features = serverFeatures(config);
|
|
729
|
-
//
|
|
730
|
-
//
|
|
422
|
+
// The sitemap needs both the flag and a `site` (absolute URLs) — only the
|
|
423
|
+
// latter deserves the remediation hint.
|
|
731
424
|
const sitemapNote = config.seo.sitemap
|
|
732
425
|
? "no (set deployment.site)"
|
|
733
426
|
: "no (seo.sitemap is false)";
|
|
@@ -738,9 +431,9 @@ const publishBuildArtifacts = async (
|
|
|
738
431
|
`Site ${config.deployment.site ?? "not set"}`,
|
|
739
432
|
`Search ${config.search.provider}`,
|
|
740
433
|
`Redirects ${config.redirects.length}`,
|
|
741
|
-
`Sitemap ${
|
|
742
|
-
`Robots ${robots ? "yes" : "no"}`,
|
|
743
|
-
`Agent JSON ${agentReadability ? "yes" : "no"}`,
|
|
434
|
+
`Sitemap ${config.deployment.site && config.seo.sitemap ? "yes" : sitemapNote}`,
|
|
435
|
+
`Robots ${config.seo.robots ? "yes" : "no"}`,
|
|
436
|
+
`Agent JSON ${config.seo.agentReadability ? "yes" : "no"}`,
|
|
744
437
|
`LLM files ${config.ai.llmsTxt.enabled ? "yes" : "no"}`,
|
|
745
438
|
`Server features ${features.length > 0 ? features.join(", ") : "none"}`,
|
|
746
439
|
].join("\n")
|
|
@@ -800,10 +493,7 @@ export const buildCommand = defineCommand({
|
|
|
800
493
|
type: "boolean",
|
|
801
494
|
},
|
|
802
495
|
},
|
|
803
|
-
meta:
|
|
804
|
-
description: "Build the docs site for production.",
|
|
805
|
-
name: "build",
|
|
806
|
-
},
|
|
496
|
+
meta: commandMeta.build,
|
|
807
497
|
async run({ args }) {
|
|
808
498
|
const root = process.cwd();
|
|
809
499
|
|
|
@@ -852,18 +542,24 @@ export const buildCommand = defineCommand({
|
|
|
852
542
|
`Building ${project.graph.pages.length} page(s) (${project.config.deployment.output} output)`
|
|
853
543
|
);
|
|
854
544
|
|
|
545
|
+
// Hand the scanned project to the integration: its `astro:build:done`
|
|
546
|
+
// hook writes the deploy artifacts (search index, llms.txt, sitemap, …)
|
|
547
|
+
// into Astro's client output during the build. An isolated build is a
|
|
548
|
+
// throwaway verify that only needs to confirm the site compiles and
|
|
549
|
+
// renders, so it publishes nothing — no network post-steps (a hosted
|
|
550
|
+
// search sync would push), no deploy artifacts.
|
|
551
|
+
if (!runtimeDir) {
|
|
552
|
+
publishBuildProject(project);
|
|
553
|
+
}
|
|
554
|
+
|
|
855
555
|
await build({
|
|
856
556
|
logLevel: "info",
|
|
857
557
|
root: project.context.outDir,
|
|
858
558
|
});
|
|
859
559
|
|
|
860
|
-
//
|
|
861
|
-
//
|
|
862
|
-
//
|
|
863
|
-
// for a real publish and would push to hosted providers. The bundle report
|
|
864
|
-
// and budget gate still run, though — `blume build --isolated --budget-js
|
|
865
|
-
// 100` exiting 0 without measuring anything would be a silent false pass
|
|
866
|
-
// in CI.
|
|
560
|
+
// The bundle report and budget gate still run for an isolated build —
|
|
561
|
+
// `blume build --isolated --budget-js 100` exiting 0 without measuring
|
|
562
|
+
// anything would be a silent false pass in CI.
|
|
867
563
|
if (runtimeDir) {
|
|
868
564
|
if (
|
|
869
565
|
project.config.deployment.output === "server" &&
|
|
@@ -918,7 +614,7 @@ export const buildCommand = defineCommand({
|
|
|
918
614
|
await emitCloudflareNegotiation(project, markdownRoutePaths(project));
|
|
919
615
|
}
|
|
920
616
|
|
|
921
|
-
await
|
|
617
|
+
await reportBuild(
|
|
922
618
|
project,
|
|
923
619
|
deployStaticDir(project.config, project.context),
|
|
924
620
|
args
|
|
@@ -6,6 +6,7 @@ import { defineCommand } from "citty";
|
|
|
6
6
|
import { join } from "pathe";
|
|
7
7
|
|
|
8
8
|
import { ensureGitignore } from "../../core/gitignore.ts";
|
|
9
|
+
import { commandMeta } from "../command-meta.ts";
|
|
9
10
|
import { refuseIfDevRunning } from "../dev-lock.ts";
|
|
10
11
|
import { logger } from "../log.ts";
|
|
11
12
|
import { prepareProject } from "../prepare.ts";
|
|
@@ -26,10 +27,7 @@ export const checkCommand = defineCommand({
|
|
|
26
27
|
type: "boolean",
|
|
27
28
|
},
|
|
28
29
|
},
|
|
29
|
-
meta:
|
|
30
|
-
description: "Type-check the docs site with astro check.",
|
|
31
|
-
name: "check",
|
|
32
|
-
},
|
|
30
|
+
meta: commandMeta.check,
|
|
33
31
|
async run({ args }) {
|
|
34
32
|
const root = process.cwd();
|
|
35
33
|
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -4,37 +4,29 @@ import { defineCommand } from "citty";
|
|
|
4
4
|
import { debounce } from "perfect-debounce";
|
|
5
5
|
|
|
6
6
|
import { generateRuntime } from "../../astro/generate.ts";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
refreshBlumeContent,
|
|
9
|
+
showBlumeErrorOverlay,
|
|
10
|
+
} from "../../astro/integration.ts";
|
|
8
11
|
import { scanProject } from "../../core/project-graph.ts";
|
|
9
12
|
import { resolveRuntimeDir } from "../../core/project.ts";
|
|
10
13
|
import { parsePort } from "../args.ts";
|
|
14
|
+
import { commandMeta } from "../command-meta.ts";
|
|
11
15
|
import {
|
|
12
16
|
acquireDevLock,
|
|
13
17
|
describeDevLock,
|
|
14
18
|
DevLockHeldError,
|
|
15
19
|
updateDevLockPort,
|
|
16
20
|
} from "../dev-lock.ts";
|
|
21
|
+
import { normalizeHost } from "../host-args.ts";
|
|
17
22
|
import { logger, reportDiagnostics } from "../log.ts";
|
|
18
23
|
import { prepareProject } from "../prepare.ts";
|
|
19
24
|
|
|
20
|
-
/**
|
|
21
|
-
* Resolve a `--host` flag value into what Astro/Vite's `server.host` expects.
|
|
22
|
-
* citty has no mixed string/boolean arg type, so `host` is declared as a
|
|
23
|
-
* string and a bare `--host` parses as `""` (the CLI entry rewrites it to
|
|
24
|
-
* `--host=` first; see `host-args.ts`) — Node would bind all interfaces
|
|
25
|
-
* for `""`, but Vite's `resolveHostname` treats it as a literal hostname and
|
|
26
|
-
* prints malformed URLs like `http://:4321/`. Match Astro's own `--host`
|
|
27
|
-
* semantics instead: bare flag → `true` (bind all interfaces), `--host
|
|
28
|
-
* 10.0.0.1` → that address, absent → `false` (localhost only).
|
|
29
|
-
*/
|
|
30
|
-
export const normalizeHost = (host: string | undefined): boolean | string =>
|
|
31
|
-
host === "" ? true : (host ?? false);
|
|
32
|
-
|
|
33
25
|
/**
|
|
34
26
|
* A fingerprint of the route set: the sorted `path entryId` pairs. It changes
|
|
35
27
|
* when a page is added, removed, or renamed (a folder rename shifts many at
|
|
36
28
|
* once) but stays equal across pure body edits — so the dev loop can tell a
|
|
37
|
-
* "structural" change (needs a
|
|
29
|
+
* "structural" change (needs a content re-sync) from a hot-reloadable one.
|
|
38
30
|
*/
|
|
39
31
|
const routeSignature = (
|
|
40
32
|
routes: readonly { entryId: string; path: string }[]
|
|
@@ -63,10 +55,7 @@ export const devCommand = defineCommand({
|
|
|
63
55
|
},
|
|
64
56
|
strict: { description: "Fail on diagnostics.", type: "boolean" },
|
|
65
57
|
},
|
|
66
|
-
meta:
|
|
67
|
-
description: "Start the Blume development server.",
|
|
68
|
-
name: "dev",
|
|
69
|
-
},
|
|
58
|
+
meta: commandMeta.dev,
|
|
70
59
|
async run({ args }) {
|
|
71
60
|
const root = process.cwd();
|
|
72
61
|
const preview = args.preview ?? false;
|
|
@@ -110,10 +99,10 @@ export const devCommand = defineCommand({
|
|
|
110
99
|
strict: args.strict,
|
|
111
100
|
});
|
|
112
101
|
|
|
113
|
-
// A factory so
|
|
114
|
-
// (route-set) change
|
|
115
|
-
//
|
|
116
|
-
//
|
|
102
|
+
// A factory so the regenerate loop can recreate the server when a
|
|
103
|
+
// structural (route-set) change can't be re-synced in place (see below).
|
|
104
|
+
// `open` is honored on first boot only — a restart must not reopen the
|
|
105
|
+
// browser.
|
|
117
106
|
const createServer = (listenPort: number | undefined, open: boolean) =>
|
|
118
107
|
dev({
|
|
119
108
|
logLevel: args.debug ? "debug" : "info",
|
|
@@ -141,14 +130,16 @@ export const devCommand = defineCommand({
|
|
|
141
130
|
let lastSignature = routeSignature(project.manifest.routes);
|
|
142
131
|
|
|
143
132
|
// Watch user inputs and regenerate the runtime data on change. A body edit
|
|
144
|
-
// hot-reloads via Vite (fast path). A route-set change
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
133
|
+
// hot-reloads via Vite (fast path). A route-set change also needs Astro's
|
|
134
|
+
// content store re-synced: its glob watcher misses directory renames, so a
|
|
135
|
+
// renamed page would 404 (`getEntry` reads a stale store). Astro hands the
|
|
136
|
+
// integration `refreshContent` for exactly that — a full loader run
|
|
137
|
+
// against the live server, after which Astro's own store watcher clears
|
|
138
|
+
// the route cache and reloads the browser. Only a server that registered
|
|
139
|
+
// no refresh (none since Astro 5) falls back to a cold restart: stop,
|
|
140
|
+
// then bring up a fresh container whose cold sync re-globs everything.
|
|
141
|
+
// perfect-debounce both debounces the watch burst (80ms) and
|
|
142
|
+
// single-flights the scan: a
|
|
152
143
|
// trigger during a run never starts a second run, only marks one trailing
|
|
153
144
|
// rerun after the current settles. Both halves are load-bearing — a plain
|
|
154
145
|
// debounce once let bursts stack overlapping scans until the heap was
|
|
@@ -166,23 +157,21 @@ export const devCommand = defineCommand({
|
|
|
166
157
|
});
|
|
167
158
|
const nextSignature = routeSignature(next.manifest.routes);
|
|
168
159
|
const structural = nextSignature !== lastSignature;
|
|
169
|
-
|
|
160
|
+
// Generate first: the new runtime data (and any staged remote content)
|
|
161
|
+
// is on disk and published before the store re-syncs against it.
|
|
162
|
+
await generateRuntime(next);
|
|
163
|
+
if (structural && !(await refreshBlumeContent())) {
|
|
170
164
|
await server.stop();
|
|
171
|
-
await generateRuntime(next);
|
|
172
165
|
server = await createServer(boundPort, false);
|
|
173
|
-
} else {
|
|
174
|
-
await generateRuntime(next);
|
|
175
166
|
}
|
|
176
167
|
// Commit the signature only after the (re)generation succeeded. If the
|
|
177
|
-
// restart above throws mid-sequence, the signature stays
|
|
178
|
-
// next watch event retries the structural path
|
|
179
|
-
// route it to the non-structural branch with the server still down.
|
|
168
|
+
// re-sync or restart above throws mid-sequence, the signature stays
|
|
169
|
+
// stale so the next watch event retries the structural path.
|
|
180
170
|
lastSignature = nextSignature;
|
|
181
171
|
// Surface any content/config errors in the terminal AND the browser
|
|
182
|
-
// overlay. The terminal report
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
// the overlay call below can be a no-op there.
|
|
172
|
+
// overlay. The terminal report is not redundant: the overlay only
|
|
173
|
+
// shows once the browser has connected, and it clears on the next HMR
|
|
174
|
+
// update.
|
|
186
175
|
reportDiagnostics(next.diagnostics, root);
|
|
187
176
|
showBlumeErrorOverlay(next.diagnostics);
|
|
188
177
|
} catch (error) {
|
|
@@ -9,6 +9,7 @@ import { packageRoot } from "../../core/package-root.ts";
|
|
|
9
9
|
import { scanProject } from "../../core/project-graph.ts";
|
|
10
10
|
import { serverFeatures } from "../../core/server-features.ts";
|
|
11
11
|
import type { Diagnostic } from "../../core/types.ts";
|
|
12
|
+
import { commandMeta } from "../command-meta.ts";
|
|
12
13
|
import { reportInternalError } from "../internal-error.ts";
|
|
13
14
|
import {
|
|
14
15
|
flushStdout,
|
|
@@ -41,10 +42,7 @@ export const doctorCommand = defineCommand({
|
|
|
41
42
|
type: "boolean",
|
|
42
43
|
},
|
|
43
44
|
},
|
|
44
|
-
meta:
|
|
45
|
-
description: "Diagnose common configuration and content problems.",
|
|
46
|
-
name: "doctor",
|
|
47
|
-
},
|
|
45
|
+
meta: commandMeta.doctor,
|
|
48
46
|
async run({ args }) {
|
|
49
47
|
const root = process.cwd();
|
|
50
48
|
const diagnostics: Diagnostic[] = [];
|