blume 1.4.0 → 1.4.2
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 +32 -0
- package/dist/cli/index.js +328 -644
- package/dist/cli/index.js.map +35 -35
- package/dist/types/core/data.d.ts +10 -0
- package/docs/configuration/ai.mdx +15 -1
- package/package.json +28 -7
- package/src/ai/component-markdown.ts +7 -6
- package/src/ai/link-headers.ts +7 -2
- package/src/astro/generate.ts +8 -13
- package/src/astro/islands.ts +4 -1
- package/src/astro/templates.ts +5 -4
- package/src/audit/checks/indexability.ts +3 -6
- package/src/audit/checks/robots.ts +18 -37
- package/src/audit/crawl.ts +49 -49
- package/src/audit/image-size.ts +13 -53
- package/src/audit/report.ts +22 -33
- package/src/audit/types.ts +6 -2
- package/src/cli/commands/dev.ts +9 -21
- package/src/cli/commands/doctor.ts +9 -22
- package/src/cli/env.ts +6 -52
- package/src/cli/init/scaffold.ts +15 -28
- package/src/cli/internal-error.ts +11 -11
- package/src/components/islands/ask-ai.tsx +25 -100
- package/src/components/islands/hooks.ts +10 -3
- package/src/components/layout/RootLayout.astro +78 -109
- package/src/components/layout/Search.astro +3 -5
- package/src/components/layout/search/types.ts +4 -16
- package/src/components/openapi/helpers.ts +21 -75
- package/src/core/component-overrides.ts +0 -7
- package/src/core/config.ts +3 -3
- package/src/core/data.ts +7 -0
- package/src/core/diagnostics.ts +10 -20
- package/src/core/fs-atomic.ts +22 -0
- package/src/core/sources/github-releases.ts +29 -26
- package/src/core/sources/mdx-remote.ts +10 -57
- package/src/core/sources/notion.ts +17 -23
- package/src/core/tsconfig-aliases.ts +39 -172
- package/src/deploy/rss.ts +4 -1
- package/src/deploy/sitemap.ts +3 -1
- package/src/eval/report.ts +20 -28
- package/src/markdown/directives.ts +6 -18
- package/src/markdown/index.ts +1 -6
- package/src/markdown/package-commands.ts +0 -4
- package/src/openapi/parse.ts +11 -9
- package/src/search/popular-icon.ts +3 -3
- package/src/translate/ledger.ts +5 -11
- package/src/translate/report.ts +22 -28
- package/src/translate/run.ts +5 -24
- package/src/translate/work-list.ts +0 -0
- package/src/deploy/xml.ts +0 -8
package/src/markdown/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
transformerNotationHighlight,
|
|
7
7
|
transformerNotationWordHighlight,
|
|
8
8
|
} from "@shikijs/transformers";
|
|
9
|
+
import { escape as escapeHtml } from "html-escaper";
|
|
9
10
|
import { codeToHtml } from "shiki";
|
|
10
11
|
|
|
11
12
|
import { baseLinksPlugin } from "./base-links.ts";
|
|
@@ -107,12 +108,6 @@ export const blumeShikiTransformers = (
|
|
|
107
108
|
return transformers;
|
|
108
109
|
};
|
|
109
110
|
|
|
110
|
-
const escapeHtml = (value: string): string =>
|
|
111
|
-
value
|
|
112
|
-
.replaceAll("&", "&")
|
|
113
|
-
.replaceAll("<", "<")
|
|
114
|
-
.replaceAll(">", ">");
|
|
115
|
-
|
|
116
111
|
/**
|
|
117
112
|
* Tag the highlighted `<pre>` with `astro-code` (plus any extra classes) so the
|
|
118
113
|
* theme's code-block styles apply — `codeToHtml`'s bare output is `pre.shiki`,
|
|
@@ -79,10 +79,6 @@ const normalizeFlags = (args: string[]): string[] =>
|
|
|
79
79
|
*/
|
|
80
80
|
const parseIntent = (input: string): Intent => {
|
|
81
81
|
const tokens = input.trim().split(WHITESPACE).filter(Boolean);
|
|
82
|
-
if (tokens.length === 0) {
|
|
83
|
-
return { args: [], operation: "install" };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
82
|
const [first, ...rest] = tokens;
|
|
87
83
|
if (first === undefined) {
|
|
88
84
|
return { args: [], operation: "install" };
|
package/src/openapi/parse.ts
CHANGED
|
@@ -150,16 +150,18 @@ const fetchSpecText = async (spec: string): Promise<string> => {
|
|
|
150
150
|
if ("text" in last) {
|
|
151
151
|
return last.text;
|
|
152
152
|
}
|
|
153
|
-
if (!last.retryable
|
|
154
|
-
|
|
153
|
+
if (!last.retryable) {
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
if (attempt < MAX_ATTEMPTS - 1) {
|
|
157
|
+
// oxlint-disable-next-line no-await-in-loop -- back off before retrying
|
|
158
|
+
await sleep(
|
|
159
|
+
Math.min(
|
|
160
|
+
last.retryAfter ?? BASE_BACKOFF_MS * 2 ** attempt,
|
|
161
|
+
MAX_RETRY_WAIT_MS
|
|
162
|
+
)
|
|
163
|
+
);
|
|
155
164
|
}
|
|
156
|
-
// oxlint-disable-next-line no-await-in-loop -- back off before retrying
|
|
157
|
-
await sleep(
|
|
158
|
-
Math.min(
|
|
159
|
-
last.retryAfter ?? BASE_BACKOFF_MS * 2 ** attempt,
|
|
160
|
-
MAX_RETRY_WAIT_MS
|
|
161
|
-
)
|
|
162
|
-
);
|
|
163
165
|
}
|
|
164
166
|
throw last.error;
|
|
165
167
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escape } from "html-escaper";
|
|
2
|
+
|
|
1
3
|
import { prefixBase } from "../components/islands/base-path.ts";
|
|
2
4
|
import { isImageIcon, isInlineSvg } from "../theme/icon-kind.ts";
|
|
3
5
|
import { resolveIcon } from "../theme/icons.ts";
|
|
@@ -21,9 +23,7 @@ export const resolvePopularIconMarkup = (
|
|
|
21
23
|
return `<span aria-hidden="true" style="display:inline-flex;width:16px;height:16px">${icon.trim()}</span>`;
|
|
22
24
|
}
|
|
23
25
|
if (isImageIcon(icon)) {
|
|
24
|
-
const src = prefixBase(base, icon.trim())
|
|
25
|
-
.replaceAll("&", "&")
|
|
26
|
-
.replaceAll('"', """);
|
|
26
|
+
const src = escape(prefixBase(base, icon.trim()));
|
|
27
27
|
return `<img src="${src}" width="16" height="16" alt="" aria-hidden="true" class="size-4" />`;
|
|
28
28
|
}
|
|
29
29
|
const resolved = resolveIcon(icon);
|
package/src/translate/ledger.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { join } from "pathe";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
7
|
+
import { writeTextAtomic } from "../core/fs-atomic.ts";
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* The committed translation ledger: which source files have been translated
|
|
9
11
|
* into which locales, and at what source content. Named "ledger" to avoid
|
|
@@ -98,15 +100,7 @@ export const writeLedger = async (
|
|
|
98
100
|
if (existing === content) {
|
|
99
101
|
return false;
|
|
100
102
|
}
|
|
101
|
-
await
|
|
102
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
103
|
-
await writeFile(tmp, content, "utf-8");
|
|
104
|
-
try {
|
|
105
|
-
await rename(tmp, path);
|
|
106
|
-
} catch (error) {
|
|
107
|
-
await rm(tmp, { force: true });
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
103
|
+
await writeTextAtomic(path, content);
|
|
110
104
|
return true;
|
|
111
105
|
};
|
|
112
106
|
|
package/src/translate/report.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { colors } from "consola/utils";
|
|
2
|
+
import type { ColorFunction } from "consola/utils";
|
|
3
|
+
|
|
1
4
|
import { AGENTS } from "../audit/agent.ts";
|
|
2
5
|
import type { AgentKind } from "../audit/agent.ts";
|
|
3
6
|
import { countBySeverity } from "../core/diagnostics.ts";
|
|
@@ -15,23 +18,15 @@ import type {
|
|
|
15
18
|
} from "./work-list.ts";
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
|
-
* The live progress UI,
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `logger.info`, which consola drops in
|
|
21
|
+
* The live progress UI, colored via consola's `colors` (which honors
|
|
22
|
+
* NO_COLOR/FORCE_COLOR and TTY detection), matching the eval report's palette.
|
|
23
|
+
* Render functions are pure; the renderer takes an injectable `write`/`now` so
|
|
24
|
+
* tests never touch a real TTY or clock. The command sends everything here to
|
|
25
|
+
* stderr via the raw `write` — never `logger.info`, which consola drops in
|
|
26
|
+
* test and CI environments.
|
|
23
27
|
*/
|
|
24
28
|
|
|
25
29
|
const ESC = String.fromCodePoint(27);
|
|
26
|
-
const COLORS = {
|
|
27
|
-
bold: `${ESC}[1m`,
|
|
28
|
-
cyan: `${ESC}[36m`,
|
|
29
|
-
dim: `${ESC}[2m`,
|
|
30
|
-
green: `${ESC}[32m`,
|
|
31
|
-
red: `${ESC}[31m`,
|
|
32
|
-
reset: `${ESC}[0m`,
|
|
33
|
-
yellow: `${ESC}[33m`,
|
|
34
|
-
};
|
|
35
30
|
|
|
36
31
|
const GLYPH: Record<TranslateItemStatus, string> = {
|
|
37
32
|
failed: "✖",
|
|
@@ -39,10 +34,10 @@ const GLYPH: Record<TranslateItemStatus, string> = {
|
|
|
39
34
|
translated: "✔",
|
|
40
35
|
};
|
|
41
36
|
|
|
42
|
-
const STATUS_COLOR: Record<TranslateItemStatus,
|
|
43
|
-
failed:
|
|
44
|
-
partial:
|
|
45
|
-
translated:
|
|
37
|
+
const STATUS_COLOR: Record<TranslateItemStatus, ColorFunction> = {
|
|
38
|
+
failed: colors.red,
|
|
39
|
+
partial: colors.yellow,
|
|
40
|
+
translated: colors.green,
|
|
46
41
|
};
|
|
47
42
|
|
|
48
43
|
export const SPINNER_FRAMES = [
|
|
@@ -96,23 +91,23 @@ export const spinnerLine = (
|
|
|
96
91
|
): string => {
|
|
97
92
|
const first = active[0] as WorkItem;
|
|
98
93
|
const more = active.length > 1 ? ` (+${active.length - 1} more)` : "";
|
|
99
|
-
return ` ${
|
|
94
|
+
return ` ${colors.cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length] as string)} ${itemLabel(first)}${more} ${colors.dim(`${done}/${total}`)}`;
|
|
100
95
|
};
|
|
101
96
|
|
|
102
97
|
/** The permanent line printed when an item finishes. */
|
|
103
98
|
export const itemEndLine = (result: TranslateItemResult): string => {
|
|
104
99
|
const color = STATUS_COLOR[result.status];
|
|
105
|
-
const glyph =
|
|
100
|
+
const glyph = color(GLYPH[result.status]);
|
|
106
101
|
const label = itemLabel(result.item);
|
|
107
102
|
if (result.status === "translated") {
|
|
108
103
|
const cells = [seconds(result.durationMs), money(result.costUsd)]
|
|
109
104
|
.filter((cell) => cell !== "")
|
|
110
105
|
.join(" ");
|
|
111
|
-
return ` ${glyph} ${label} ${
|
|
106
|
+
return ` ${glyph} ${label} ${colors.dim(cells)}`;
|
|
112
107
|
}
|
|
113
108
|
const word = result.status === "partial" ? "partial" : "failed";
|
|
114
|
-
return ` ${glyph} ${label} ${color
|
|
115
|
-
result.detail ?
|
|
109
|
+
return ` ${glyph} ${label} ${color(word)}${
|
|
110
|
+
result.detail ? colors.dim(`: ${result.detail}`) : ""
|
|
116
111
|
}`;
|
|
117
112
|
};
|
|
118
113
|
|
|
@@ -122,7 +117,7 @@ export const translateHeaderLine = (
|
|
|
122
117
|
localeCount: number,
|
|
123
118
|
agent: AgentKind
|
|
124
119
|
): string =>
|
|
125
|
-
`${
|
|
120
|
+
`${colors.bold("blume translate")} ${itemCount} item(s) · ${localeCount} locale(s) · ${AGENTS[agent].name}`;
|
|
126
121
|
|
|
127
122
|
/**
|
|
128
123
|
* `Translated 11 files into 2 locales · 1 failed · 2 adopted · 8 already up to
|
|
@@ -148,8 +143,7 @@ export const translateSummaryLine = (
|
|
|
148
143
|
/** Dim warnings for work-list diagnostics (e.g. a factory-form meta file). */
|
|
149
144
|
export const diagnosticLines = (diagnostics: Diagnostic[]): string[] =>
|
|
150
145
|
diagnostics.map(
|
|
151
|
-
(diagnostic) =>
|
|
152
|
-
` ${COLORS.yellow}⚠${COLORS.reset} ${COLORS.dim}${diagnostic.message}${COLORS.reset}`
|
|
146
|
+
(diagnostic) => ` ${colors.yellow("⚠")} ${colors.dim(diagnostic.message)}`
|
|
153
147
|
);
|
|
154
148
|
|
|
155
149
|
/** Every (source, locale, status) drift row in a work list, pages then meta. */
|
|
@@ -176,11 +170,11 @@ const driftRows = (
|
|
|
176
170
|
export const checkLines = (workList: TranslateWorkList): string[] => [
|
|
177
171
|
...driftRows(workList).map(
|
|
178
172
|
(row) =>
|
|
179
|
-
` ${
|
|
173
|
+
` ${colors.red("✖")} ${row.sourceRel} → ${row.locale} ${colors.dim(row.status)}`
|
|
180
174
|
),
|
|
181
175
|
...workList.untracked.map(
|
|
182
176
|
(entry) =>
|
|
183
|
-
` ${
|
|
177
|
+
` ${colors.dim(`⊘ ${entry.sourceRel} → ${entry.locale} untracked (adopted by the next translate run)`)}`
|
|
184
178
|
),
|
|
185
179
|
];
|
|
186
180
|
|
package/src/translate/run.ts
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
mkdir,
|
|
4
|
-
mkdtemp,
|
|
5
|
-
readFile,
|
|
6
|
-
rename,
|
|
7
|
-
rm,
|
|
8
|
-
writeFile,
|
|
9
|
-
} from "node:fs/promises";
|
|
2
|
+
import { mkdtemp, readFile } from "node:fs/promises";
|
|
10
3
|
import { tmpdir } from "node:os";
|
|
11
4
|
|
|
12
|
-
import {
|
|
5
|
+
import { join } from "pathe";
|
|
13
6
|
|
|
14
7
|
import { AGENTS, WINDOWS_COMMAND_NOT_FOUND } from "../audit/agent.ts";
|
|
15
8
|
import type { AgentKind } from "../audit/agent.ts";
|
|
9
|
+
import { writeTextAtomic } from "../core/fs-atomic.ts";
|
|
16
10
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
17
11
|
import type { LocaleConfig } from "../core/schema.ts";
|
|
18
12
|
import type { Diagnostic } from "../core/types.ts";
|
|
@@ -94,19 +88,6 @@ interface RunContext {
|
|
|
94
88
|
/** The root directory's key in a meta-titles prompt (an empty key is opaque). */
|
|
95
89
|
const metaDirKey = (dir: string): string => (dir === "" ? "." : dir);
|
|
96
90
|
|
|
97
|
-
/** Write atomically (temp + rename) so a watcher never sees a partial file. */
|
|
98
|
-
const writeFileAtomic = async (path: string, text: string): Promise<void> => {
|
|
99
|
-
await mkdir(dirname(path), { recursive: true });
|
|
100
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
101
|
-
await writeFile(tmp, text, "utf-8");
|
|
102
|
-
try {
|
|
103
|
-
await rename(tmp, path);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
await rm(tmp, { force: true });
|
|
106
|
-
throw error;
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
91
|
/**
|
|
111
92
|
* One headless agent call. A Windows shell launch reports a missing executable
|
|
112
93
|
* through exit code 9009 instead of a spawn error, so that is normalized to
|
|
@@ -172,7 +153,7 @@ const runPageItem = async (
|
|
|
172
153
|
if (!validated.ok) {
|
|
173
154
|
return done("failed", validated.reason, output.costUsd);
|
|
174
155
|
}
|
|
175
|
-
await
|
|
156
|
+
await writeTextAtomic(item.targetPath, validated.text);
|
|
176
157
|
stampLedger(ledger, item.sourceRel, item.locale, hashSource(sourceText));
|
|
177
158
|
return done("translated", undefined, output.costUsd);
|
|
178
159
|
};
|
|
@@ -218,7 +199,7 @@ const runMetaItem = async (
|
|
|
218
199
|
// Each entry writes and stamps independently, so a partially usable reply
|
|
219
200
|
// still lands the titles it did translate.
|
|
220
201
|
// oxlint-disable-next-line no-await-in-loop
|
|
221
|
-
await
|
|
202
|
+
await writeTextAtomic(
|
|
222
203
|
entry.targetPath,
|
|
223
204
|
generateMetaModule(entry.meta.data, translated)
|
|
224
205
|
);
|
|
Binary file
|
package/src/deploy/xml.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/** Escape a string for safe inclusion in XML text or attribute content. */
|
|
2
|
-
export const escapeXml = (value: string): string =>
|
|
3
|
-
value
|
|
4
|
-
.replaceAll("&", "&")
|
|
5
|
-
.replaceAll("<", "<")
|
|
6
|
-
.replaceAll(">", ">")
|
|
7
|
-
.replaceAll('"', """)
|
|
8
|
-
.replaceAll("'", "'");
|