mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-30 22:12:32 +08:00
build(deps): trim runtime dependency graph
This commit is contained in:
@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Changes
|
||||
|
||||
- Plugin SDK: mark remaining legacy alias exports and diffs tool/config aliases with deprecation metadata, and add a guard so future legacy alias comments require `@deprecated` tags. Thanks @vincentkoc.
|
||||
- CLI/QR/dependencies: internalize small terminal progress and QR wrapper helpers while keeping the real QR encoder dependency direct, reducing the default runtime dependency graph without changing QR output behavior. Thanks @vincentkoc.
|
||||
- Channels: add Yuanbao channel docs entrance so the Tencent Yuanbao bot appears in the channel listing and sidebar navigation. (#73443) Thanks @loongfay.
|
||||
- Active Memory: add optional per-conversation `allowedChatIds` and `deniedChatIds` filters so operators can enable recall only for selected direct, group, or channel conversations while keeping broad sessions skipped. (#67977) Thanks @quengh.
|
||||
- Active Memory: return bounded partial recall summaries when the hidden memory sub-agent times out, including the default temporary-transcript path, so useful recovered context is not discarded. (#73219) Thanks @joeykrug.
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
"express": "5.2.1",
|
||||
"playwright-core": "1.59.1",
|
||||
"typebox": "1.1.33",
|
||||
"undici": "8.1.0",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
"undici": "8.1.0"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
"private": true,
|
||||
"description": "OpenClaw Deepgram media-understanding provider",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
"private": true,
|
||||
"description": "OpenClaw ElevenLabs speech plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
"private": true,
|
||||
"description": "OpenClaw Mistral provider plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "0.70.5",
|
||||
"typebox": "1.1.33",
|
||||
"ws": "^8.20.0"
|
||||
"typebox": "1.1.33"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
|
||||
@@ -1605,7 +1605,6 @@
|
||||
"@mariozechner/pi-coding-agent": "0.70.5",
|
||||
"@mariozechner/pi-tui": "0.70.5",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@vincentkoc/qrcode-tui": "0.2.1",
|
||||
"ajv": "^8.20.0",
|
||||
"chalk": "^5.6.2",
|
||||
"chokidar": "^5.0.0",
|
||||
@@ -1621,8 +1620,8 @@
|
||||
"jszip": "^3.10.1",
|
||||
"markdown-it": "14.1.1",
|
||||
"openai": "^6.34.0",
|
||||
"osc-progress": "^0.3.0",
|
||||
"proxy-agent": "^8.0.1",
|
||||
"qrcode": "1.5.4",
|
||||
"semver": "7.7.4",
|
||||
"sqlite-vec": "0.1.9",
|
||||
"tar": "7.5.13",
|
||||
|
||||
48
pnpm-lock.yaml
generated
48
pnpm-lock.yaml
generated
@@ -66,9 +66,6 @@ importers:
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: 1.29.0
|
||||
version: 1.29.0(zod@4.3.6)
|
||||
'@vincentkoc/qrcode-tui':
|
||||
specifier: 0.2.1
|
||||
version: 0.2.1
|
||||
ajv:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
@@ -114,12 +111,12 @@ importers:
|
||||
openai:
|
||||
specifier: ^6.34.0
|
||||
version: 6.34.0(ws@8.20.0)(zod@4.3.6)
|
||||
osc-progress:
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0
|
||||
proxy-agent:
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1
|
||||
qrcode:
|
||||
specifier: 1.5.4
|
||||
version: 1.5.4
|
||||
semver:
|
||||
specifier: 7.7.4
|
||||
version: 7.7.4
|
||||
@@ -356,9 +353,6 @@ importers:
|
||||
typebox:
|
||||
specifier: 1.1.33
|
||||
version: 1.1.33
|
||||
undici:
|
||||
specifier: 8.1.0
|
||||
version: 8.1.0
|
||||
ws:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
@@ -366,6 +360,9 @@ importers:
|
||||
'@openclaw/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/plugin-sdk
|
||||
undici:
|
||||
specifier: 8.1.0
|
||||
version: 8.1.0
|
||||
|
||||
extensions/byteplus:
|
||||
devDependencies:
|
||||
@@ -426,10 +423,6 @@ importers:
|
||||
version: link:../../packages/plugin-sdk
|
||||
|
||||
extensions/deepgram:
|
||||
dependencies:
|
||||
ws:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
devDependencies:
|
||||
'@openclaw/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
@@ -566,10 +559,6 @@ importers:
|
||||
version: link:../../packages/plugin-sdk
|
||||
|
||||
extensions/elevenlabs:
|
||||
dependencies:
|
||||
ws:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
devDependencies:
|
||||
'@openclaw/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
@@ -946,10 +935,6 @@ importers:
|
||||
version: link:../../packages/plugin-sdk
|
||||
|
||||
extensions/mistral:
|
||||
dependencies:
|
||||
ws:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
devDependencies:
|
||||
'@openclaw/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
@@ -1492,13 +1477,13 @@ importers:
|
||||
typebox:
|
||||
specifier: 1.1.33
|
||||
version: 1.1.33
|
||||
ws:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
devDependencies:
|
||||
'@openclaw/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/plugin-sdk
|
||||
ws:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
|
||||
extensions/xiaomi:
|
||||
devDependencies:
|
||||
@@ -4285,11 +4270,6 @@ packages:
|
||||
resolution: {integrity: sha512-N8/FHc/lmlMDCumMuTXyRHCxlov5KZY6unmJ9QR2GOw+OpROZMBsXYGwE+ZMtvN21ql9+Xb8KhGNBj08IrG3Wg==}
|
||||
engines: {node: '>=16', npm: '>=8'}
|
||||
|
||||
'@vincentkoc/qrcode-tui@0.2.1':
|
||||
resolution: {integrity: sha512-F2XVHMfasJ0q8G93gtcyU9Px0wMH6o6nIZLrZYSHc6dm9Pq3oCbHuVYYG/UQvJD0rhrGH3P9B6qgpCAqSDUw5w==}
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
'@vitest/browser-playwright@4.1.5':
|
||||
resolution: {integrity: sha512-CWy0lBQJq97nionyJJdnaU4961IXTl43a7UCu5nHy51IoKxAt6PVIJLo+76rVl7KOOgcWHNkG4kbJu/pW7knvA==}
|
||||
peerDependencies:
|
||||
@@ -6401,10 +6381,6 @@ packages:
|
||||
opusscript@0.1.1:
|
||||
resolution: {integrity: sha512-mL0fZZOUnXdZ78woRXp18lApwpp0lF5tozJOD1Wut0dgrA9WuQTgSels/CSmFleaAZrJi/nci5KOVtbuxeWoQA==}
|
||||
|
||||
osc-progress@0.3.0:
|
||||
resolution: {integrity: sha512-4/8JfsetakdeEa4vAYV45FW20aY+B/+K8NEXp5Eiar3wR8726whgHrbSg5Ar/ZY1FLJ/AGtUqV7W2IVF+Gvp9A==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
oxfmt@0.46.0:
|
||||
resolution: {integrity: sha512-CopwJOwPAjZ9p76fCvz+mSOJTw9/NY3cSksZK3VO/bUQ8UoEcketNgUuYS0UB3p+R9XnXe7wGGXUmyFxc7QxJA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -11061,10 +11037,6 @@ snapshots:
|
||||
|
||||
'@urbit/aura@3.0.0': {}
|
||||
|
||||
'@vincentkoc/qrcode-tui@0.2.1':
|
||||
dependencies:
|
||||
qrcode: 1.5.4
|
||||
|
||||
'@vitest/browser-playwright@4.1.5(playwright@1.59.1)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.5)':
|
||||
dependencies:
|
||||
'@vitest/browser': 4.1.5(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.5)
|
||||
@@ -13571,8 +13543,6 @@ snapshots:
|
||||
|
||||
opusscript@0.1.1: {}
|
||||
|
||||
osc-progress@0.3.0: {}
|
||||
|
||||
oxfmt@0.46.0:
|
||||
dependencies:
|
||||
tinypool: 2.1.0
|
||||
|
||||
@@ -21,5 +21,5 @@ docker_build_run qr-import-build \
|
||||
-f "$ROOT_DIR/scripts/e2e/Dockerfile.qr-import" \
|
||||
"$ROOT_DIR"
|
||||
|
||||
echo "Running qrcode-tui import smoke..."
|
||||
run_logged qr-import-run docker run --rm -t "$IMAGE_NAME" node -e "import('@vincentkoc/qrcode-tui').then(async (m)=>{process.stdout.write(await m.renderTerminal('qr-smoke',{small:true}))})"
|
||||
echo "Running qrcode import smoke..."
|
||||
run_logged qr-import-run docker run --rm -t "$IMAGE_NAME" node -e "import('qrcode').then(async (m)=>{const q=m.default??m;process.stdout.write(await q.toString('qr-smoke',{small:true,type:'terminal'}))})"
|
||||
|
||||
@@ -56,11 +56,6 @@
|
||||
],
|
||||
"risk": ["native", "parser", "untrusted-files"]
|
||||
},
|
||||
"@vincentkoc/qrcode-tui": {
|
||||
"owner": "core:qr-setup",
|
||||
"class": "default-runtime-initially",
|
||||
"risk": ["terminal-rendering"]
|
||||
},
|
||||
"ajv": {
|
||||
"owner": "core:json-schema-validation",
|
||||
"class": "core-runtime",
|
||||
@@ -142,11 +137,6 @@
|
||||
"class": "default-runtime-initially",
|
||||
"risk": ["provider-sdk", "network"]
|
||||
},
|
||||
"osc-progress": {
|
||||
"owner": "core:terminal-progress",
|
||||
"class": "core-runtime",
|
||||
"risk": ["terminal-rendering"]
|
||||
},
|
||||
"pdfjs-dist": {
|
||||
"owner": "plugin:document-extract",
|
||||
"class": "plugin-runtime",
|
||||
@@ -158,6 +148,11 @@
|
||||
"class": "core-runtime",
|
||||
"risk": ["network", "proxy"]
|
||||
},
|
||||
"qrcode": {
|
||||
"owner": "core:qr-setup",
|
||||
"class": "default-runtime-initially",
|
||||
"risk": ["terminal-rendering", "png-encoding"]
|
||||
},
|
||||
"semver": {
|
||||
"owner": "core:package-versioning",
|
||||
"class": "core-runtime",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { spinner } from "@clack/prompts";
|
||||
import { createOscProgressController, supportsOscProgress } from "osc-progress";
|
||||
import { createOscProgressController, supportsOscProgress } from "../terminal/osc-progress.js";
|
||||
import {
|
||||
clearActiveProgressLine,
|
||||
registerActiveProgressLine,
|
||||
|
||||
@@ -3,16 +3,20 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { MOCK_PNG_BASE64, renderPngBase64 } = vi.hoisted(() => {
|
||||
const { MOCK_PNG_BASE64, MOCK_PNG_DATA_URL, toDataURL } = vi.hoisted(() => {
|
||||
const MOCK_PNG_BASE64 = "ZmFrZXBuZw==";
|
||||
const MOCK_PNG_DATA_URL = `data:image/png;base64,${MOCK_PNG_BASE64}`;
|
||||
return {
|
||||
MOCK_PNG_BASE64,
|
||||
renderPngBase64: vi.fn(async () => MOCK_PNG_BASE64),
|
||||
MOCK_PNG_DATA_URL,
|
||||
toDataURL: vi.fn(async () => MOCK_PNG_DATA_URL),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@vincentkoc/qrcode-tui", () => ({
|
||||
renderPngBase64,
|
||||
vi.mock("qrcode", () => ({
|
||||
default: {
|
||||
toDataURL,
|
||||
},
|
||||
}));
|
||||
|
||||
import {
|
||||
@@ -26,36 +30,40 @@ describe("renderQrPngBase64", () => {
|
||||
const tmpRoot = path.join(os.tmpdir(), "openclaw-qr-image-tests");
|
||||
|
||||
beforeEach(() => {
|
||||
renderPngBase64.mockClear();
|
||||
toDataURL.mockClear();
|
||||
toDataURL.mockResolvedValue(MOCK_PNG_DATA_URL);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(tmpRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("delegates PNG rendering to qrcode-tui", async () => {
|
||||
it("delegates PNG rendering to qrcode", async () => {
|
||||
await expect(renderQrPngBase64("openclaw", { scale: 8, marginModules: 2 })).resolves.toBe(
|
||||
MOCK_PNG_BASE64,
|
||||
);
|
||||
expect(renderPngBase64).toHaveBeenCalledWith("openclaw", {
|
||||
expect(toDataURL).toHaveBeenCalledWith("openclaw", {
|
||||
margin: 2,
|
||||
scale: 8,
|
||||
type: "image/png",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default PNG rendering options", async () => {
|
||||
await renderQrPngBase64("openclaw");
|
||||
expect(renderPngBase64).toHaveBeenCalledWith("openclaw", {
|
||||
expect(toDataURL).toHaveBeenCalledWith("openclaw", {
|
||||
margin: 4,
|
||||
scale: 6,
|
||||
type: "image/png",
|
||||
});
|
||||
});
|
||||
|
||||
it("floors finite PNG rendering options before delegating", async () => {
|
||||
await renderQrPngBase64("openclaw", { scale: 8.9, marginModules: 2.9 });
|
||||
expect(renderPngBase64).toHaveBeenCalledWith("openclaw", {
|
||||
expect(toDataURL).toHaveBeenCalledWith("openclaw", {
|
||||
margin: 2,
|
||||
scale: 8,
|
||||
type: "image/png",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,7 +76,14 @@ describe("renderQrPngBase64", () => {
|
||||
["marginModules", 6, Number.POSITIVE_INFINITY, "marginModules must be a finite number."],
|
||||
])("rejects invalid %s values", async (_name, scale, marginModules, message) => {
|
||||
await expect(renderQrPngBase64("openclaw", { scale, marginModules })).rejects.toThrow(message);
|
||||
expect(renderPngBase64).not.toHaveBeenCalled();
|
||||
expect(toDataURL).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects non-PNG qrcode data URLs", async () => {
|
||||
toDataURL.mockResolvedValue("data:image/svg+xml;base64,PHN2Zz4=");
|
||||
await expect(renderQrPngBase64("openclaw")).rejects.toThrow(
|
||||
"Expected qrcode to return a PNG data URL.",
|
||||
);
|
||||
});
|
||||
|
||||
it("formats QR PNG data URLs", async () => {
|
||||
@@ -104,6 +119,6 @@ describe("renderQrPngBase64", () => {
|
||||
fileName: opts.fileName,
|
||||
}),
|
||||
).rejects.toThrow(`${name} must be a non-empty filename segment.`);
|
||||
expect(renderPngBase64).not.toHaveBeenCalled();
|
||||
expect(toDataURL).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { loadQrCodeTuiRuntime } from "./qr-runtime.ts";
|
||||
import { loadQrCodeRuntime, normalizeQrText } from "./qr-runtime.ts";
|
||||
|
||||
const DEFAULT_QR_PNG_SCALE = 6;
|
||||
const DEFAULT_QR_PNG_MARGIN_MODULES = 4;
|
||||
@@ -72,11 +72,16 @@ export async function renderQrPngBase64(
|
||||
min: MIN_QR_PNG_MARGIN_MODULES,
|
||||
max: MAX_QR_PNG_MARGIN_MODULES,
|
||||
});
|
||||
const { renderPngBase64 } = await loadQrCodeTuiRuntime();
|
||||
return await renderPngBase64(input, {
|
||||
const qrCode = await loadQrCodeRuntime();
|
||||
const dataUrl = await qrCode.toDataURL(normalizeQrText(input), {
|
||||
margin: marginModules,
|
||||
scale,
|
||||
type: "image/png",
|
||||
});
|
||||
if (!dataUrl.startsWith(QR_PNG_DATA_URL_PREFIX)) {
|
||||
throw new Error("Expected qrcode to return a PNG data URL.");
|
||||
}
|
||||
return dataUrl.slice(QR_PNG_DATA_URL_PREFIX.length);
|
||||
}
|
||||
|
||||
export function formatQrPngDataUrl(base64: string): string {
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
let qrCodeTuiRuntimePromise: Promise<typeof import("@vincentkoc/qrcode-tui")> | null = null;
|
||||
import type QRCode from "qrcode";
|
||||
|
||||
export async function loadQrCodeTuiRuntime() {
|
||||
if (!qrCodeTuiRuntimePromise) {
|
||||
qrCodeTuiRuntimePromise = import("@vincentkoc/qrcode-tui");
|
||||
type QrCodeRuntime = typeof QRCode;
|
||||
|
||||
let qrCodeRuntimePromise: Promise<QrCodeRuntime> | null = null;
|
||||
|
||||
export async function loadQrCodeRuntime(): Promise<QrCodeRuntime> {
|
||||
if (!qrCodeRuntimePromise) {
|
||||
qrCodeRuntimePromise = import("qrcode").then((mod) => mod.default ?? mod);
|
||||
}
|
||||
return await qrCodeTuiRuntimePromise;
|
||||
return await qrCodeRuntimePromise;
|
||||
}
|
||||
|
||||
export function normalizeQrText(text: string): string {
|
||||
if (typeof text !== "string") {
|
||||
throw new TypeError("QR text must be a string.");
|
||||
}
|
||||
if (text.length === 0) {
|
||||
throw new Error("QR text must not be empty.");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
28
src/media/qr-terminal.test.ts
Normal file
28
src/media/qr-terminal.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { toString } = vi.hoisted(() => ({
|
||||
toString: vi.fn(async () => "ASCII-QR"),
|
||||
}));
|
||||
|
||||
vi.mock("qrcode", () => ({
|
||||
default: {
|
||||
toString,
|
||||
},
|
||||
}));
|
||||
|
||||
import { renderQrTerminal } from "./qr-terminal.ts";
|
||||
|
||||
describe("renderQrTerminal", () => {
|
||||
it("delegates terminal rendering to qrcode", async () => {
|
||||
await expect(renderQrTerminal("openclaw")).resolves.toBe("ASCII-QR");
|
||||
expect(toString).toHaveBeenCalledWith("openclaw", {
|
||||
small: true,
|
||||
type: "terminal",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects empty QR text", async () => {
|
||||
await expect(renderQrTerminal("")).rejects.toThrow("QR text must not be empty.");
|
||||
expect(toString).not.toHaveBeenCalledWith("", expect.anything());
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,12 @@
|
||||
import { loadQrCodeTuiRuntime } from "./qr-runtime.ts";
|
||||
import { loadQrCodeRuntime, normalizeQrText } from "./qr-runtime.ts";
|
||||
|
||||
export async function renderQrTerminal(
|
||||
input: string,
|
||||
opts: { small?: boolean } = {},
|
||||
): Promise<string> {
|
||||
const { renderTerminal } = await loadQrCodeTuiRuntime();
|
||||
return await renderTerminal(input, { small: opts.small ?? true });
|
||||
const qrCode = await loadQrCodeRuntime();
|
||||
return await qrCode.toString(normalizeQrText(input), {
|
||||
small: opts.small ?? true,
|
||||
type: "terminal",
|
||||
});
|
||||
}
|
||||
|
||||
29
src/terminal/osc-progress.test.ts
Normal file
29
src/terminal/osc-progress.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createOscProgressController, supportsOscProgress } from "./osc-progress.js";
|
||||
|
||||
describe("OSC progress", () => {
|
||||
it("detects supported terminal environments", () => {
|
||||
expect(supportsOscProgress({ TERM_PROGRAM: "WezTerm" }, true)).toBe(true);
|
||||
expect(supportsOscProgress({ TERM_PROGRAM: "Apple_Terminal" }, true)).toBe(false);
|
||||
expect(supportsOscProgress({ WT_SESSION: "1" }, false)).toBe(false);
|
||||
});
|
||||
|
||||
it("writes sanitized OSC 9;4 progress sequences", () => {
|
||||
const writes: string[] = [];
|
||||
const controller = createOscProgressController({
|
||||
env: { TERM_PROGRAM: "ghostty" },
|
||||
isTty: true,
|
||||
write: (chunk) => writes.push(chunk),
|
||||
});
|
||||
|
||||
controller.setIndeterminate("Build\u001b]bad\u0007");
|
||||
controller.setPercent("Build", 42.6);
|
||||
controller.clear();
|
||||
|
||||
expect(writes).toEqual([
|
||||
"\u001b]9;4;3;;Buildbad\u001b\\",
|
||||
"\u001b]9;4;1;43;Build\u001b\\",
|
||||
"\u001b]9;4;0;0;Build\u001b\\",
|
||||
]);
|
||||
});
|
||||
});
|
||||
70
src/terminal/osc-progress.ts
Normal file
70
src/terminal/osc-progress.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
const OSC_PROGRESS_PREFIX = "\u001b]9;4;";
|
||||
const OSC_PROGRESS_ST = "\u001b\\";
|
||||
const OSC_PROGRESS_BEL = "\u0007";
|
||||
const OSC_PROGRESS_C1_ST = "\u009c";
|
||||
|
||||
export type OscProgressController = {
|
||||
setIndeterminate: (label: string) => void;
|
||||
setPercent: (label: string, percent: number) => void;
|
||||
clear: () => void;
|
||||
};
|
||||
|
||||
export function supportsOscProgress(env: NodeJS.ProcessEnv, isTty: boolean): boolean {
|
||||
if (!isTty) {
|
||||
return false;
|
||||
}
|
||||
const termProgram = (env.TERM_PROGRAM ?? "").toLowerCase();
|
||||
return (
|
||||
termProgram.includes("ghostty") || termProgram.includes("wezterm") || Boolean(env.WT_SESSION)
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeOscProgressLabel(label: string): string {
|
||||
return label
|
||||
.replaceAll(OSC_PROGRESS_ST, "")
|
||||
.replaceAll(OSC_PROGRESS_BEL, "")
|
||||
.replaceAll(OSC_PROGRESS_C1_ST, "")
|
||||
.split("\u001b")
|
||||
.join("")
|
||||
.replaceAll("]", "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function formatOscProgress(state: number, percent: number | null, label: string): string {
|
||||
const cleanLabel = sanitizeOscProgressLabel(label);
|
||||
if (percent === null) {
|
||||
return `${OSC_PROGRESS_PREFIX}${state};;${cleanLabel}${OSC_PROGRESS_ST}`;
|
||||
}
|
||||
const normalizedPercent = Math.max(0, Math.min(100, Math.round(percent)));
|
||||
return `${OSC_PROGRESS_PREFIX}${state};${normalizedPercent};${cleanLabel}${OSC_PROGRESS_ST}`;
|
||||
}
|
||||
|
||||
export function createOscProgressController(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
isTty: boolean;
|
||||
write: (chunk: string) => void;
|
||||
}): OscProgressController {
|
||||
if (!supportsOscProgress(params.env, params.isTty)) {
|
||||
return {
|
||||
setIndeterminate: () => {},
|
||||
setPercent: () => {},
|
||||
clear: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
let lastLabel = "";
|
||||
|
||||
return {
|
||||
setIndeterminate: (label: string) => {
|
||||
lastLabel = label;
|
||||
params.write(formatOscProgress(3, null, label));
|
||||
},
|
||||
setPercent: (label: string, percent: number) => {
|
||||
lastLabel = label;
|
||||
params.write(formatOscProgress(1, percent, label));
|
||||
},
|
||||
clear: () => {
|
||||
params.write(formatOscProgress(0, 0, lastLabel));
|
||||
},
|
||||
};
|
||||
}
|
||||
16
src/types/osc-progress.d.ts
vendored
16
src/types/osc-progress.d.ts
vendored
@@ -1,16 +0,0 @@
|
||||
declare module "osc-progress" {
|
||||
export type OscProgressController = {
|
||||
setIndeterminate: (label: string) => void;
|
||||
setPercent: (label: string, percent: number) => void;
|
||||
clear: () => void;
|
||||
done?: () => void;
|
||||
};
|
||||
|
||||
export function createOscProgressController(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
isTty: boolean;
|
||||
write: (chunk: string) => void;
|
||||
}): OscProgressController;
|
||||
|
||||
export function supportsOscProgress(env: NodeJS.ProcessEnv, isTty: boolean): boolean;
|
||||
}
|
||||
42
src/types/qrcode.d.ts
vendored
Normal file
42
src/types/qrcode.d.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
declare module "qrcode" {
|
||||
export type QrCodeErrorCorrectionLevel =
|
||||
| "L"
|
||||
| "M"
|
||||
| "Q"
|
||||
| "H"
|
||||
| "low"
|
||||
| "medium"
|
||||
| "quartile"
|
||||
| "high";
|
||||
|
||||
export type QrCodeColorOptions = {
|
||||
dark?: string;
|
||||
light?: string;
|
||||
};
|
||||
|
||||
export type QrCodeRenderOptions = {
|
||||
color?: QrCodeColorOptions;
|
||||
errorCorrectionLevel?: QrCodeErrorCorrectionLevel;
|
||||
margin?: number;
|
||||
scale?: number;
|
||||
small?: boolean;
|
||||
type?: "image/png" | "png" | "svg" | "terminal" | "utf8";
|
||||
width?: number;
|
||||
};
|
||||
|
||||
export function toString(text: string, options?: QrCodeRenderOptions): Promise<string>;
|
||||
export function toDataURL(text: string, options?: QrCodeRenderOptions): Promise<string>;
|
||||
export function toFile(
|
||||
filePath: string,
|
||||
text: string,
|
||||
options?: QrCodeRenderOptions,
|
||||
): Promise<void>;
|
||||
|
||||
const qrcode: {
|
||||
toString: typeof toString;
|
||||
toDataURL: typeof toDataURL;
|
||||
toFile: typeof toFile;
|
||||
};
|
||||
|
||||
export default qrcode;
|
||||
}
|
||||
Reference in New Issue
Block a user