From 7e5d6dba80016a5d3502fa6cfc5589aa58a072af Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 29 Apr 2026 08:11:34 +0100 Subject: [PATCH] build(deps): trim runtime dependency graph --- CHANGELOG.md | 1 + extensions/browser/package.json | 4 +- extensions/deepgram/package.json | 3 -- extensions/elevenlabs/package.json | 3 -- extensions/mistral/package.json | 3 -- extensions/xai/package.json | 6 +-- package.json | 3 +- pnpm-lock.yaml | 48 ++++-------------- scripts/e2e/qr-import-docker.sh | 4 +- scripts/lib/dependency-ownership.json | 15 ++---- src/cli/progress.ts | 2 +- src/media/qr-image.test.ts | 37 +++++++++----- src/media/qr-image.ts | 11 +++-- src/media/qr-runtime.ts | 24 +++++++-- src/media/qr-terminal.test.ts | 28 +++++++++++ src/media/qr-terminal.ts | 9 ++-- src/terminal/osc-progress.test.ts | 29 +++++++++++ src/terminal/osc-progress.ts | 70 +++++++++++++++++++++++++++ src/types/osc-progress.d.ts | 16 ------ src/types/qrcode.d.ts | 42 ++++++++++++++++ 20 files changed, 252 insertions(+), 106 deletions(-) create mode 100644 src/media/qr-terminal.test.ts create mode 100644 src/terminal/osc-progress.test.ts create mode 100644 src/terminal/osc-progress.ts delete mode 100644 src/types/osc-progress.d.ts create mode 100644 src/types/qrcode.d.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c816a6d6f3..307a9e18004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/browser/package.json b/extensions/browser/package.json index 11f6ae4fd66..81613b79512 100644 --- a/extensions/browser/package.json +++ b/extensions/browser/package.json @@ -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": [ diff --git a/extensions/deepgram/package.json b/extensions/deepgram/package.json index 3158c54022b..e63d200fc42 100644 --- a/extensions/deepgram/package.json +++ b/extensions/deepgram/package.json @@ -4,9 +4,6 @@ "private": true, "description": "OpenClaw Deepgram media-understanding provider", "type": "module", - "dependencies": { - "ws": "^8.20.0" - }, "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" }, diff --git a/extensions/elevenlabs/package.json b/extensions/elevenlabs/package.json index 2e554b2273d..b169b4ca77b 100644 --- a/extensions/elevenlabs/package.json +++ b/extensions/elevenlabs/package.json @@ -4,9 +4,6 @@ "private": true, "description": "OpenClaw ElevenLabs speech plugin", "type": "module", - "dependencies": { - "ws": "^8.20.0" - }, "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" }, diff --git a/extensions/mistral/package.json b/extensions/mistral/package.json index f7d4199cf6b..d8314c3b83f 100644 --- a/extensions/mistral/package.json +++ b/extensions/mistral/package.json @@ -4,9 +4,6 @@ "private": true, "description": "OpenClaw Mistral provider plugin", "type": "module", - "dependencies": { - "ws": "^8.20.0" - }, "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" }, diff --git a/extensions/xai/package.json b/extensions/xai/package.json index 687a0915730..3b32c44cdbd 100644 --- a/extensions/xai/package.json +++ b/extensions/xai/package.json @@ -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": [ diff --git a/package.json b/package.json index 52bd8315a93..64a5876780e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6758a1e05b..b83a5f10607 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/scripts/e2e/qr-import-docker.sh b/scripts/e2e/qr-import-docker.sh index 6f9f99bdfc0..d1f07d58c0b 100755 --- a/scripts/e2e/qr-import-docker.sh +++ b/scripts/e2e/qr-import-docker.sh @@ -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'}))})" diff --git a/scripts/lib/dependency-ownership.json b/scripts/lib/dependency-ownership.json index 63f45a99167..f7d1f2f0a8a 100644 --- a/scripts/lib/dependency-ownership.json +++ b/scripts/lib/dependency-ownership.json @@ -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", diff --git a/src/cli/progress.ts b/src/cli/progress.ts index 0f4f50df4b1..4ec8f58211a 100644 --- a/src/cli/progress.ts +++ b/src/cli/progress.ts @@ -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, diff --git a/src/media/qr-image.test.ts b/src/media/qr-image.test.ts index f86bc0225f5..5c83791bbfe 100644 --- a/src/media/qr-image.test.ts +++ b/src/media/qr-image.test.ts @@ -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(); }); }); diff --git a/src/media/qr-image.ts b/src/media/qr-image.ts index 536b515f97c..d08b2fcb1da 100644 --- a/src/media/qr-image.ts +++ b/src/media/qr-image.ts @@ -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 { diff --git a/src/media/qr-runtime.ts b/src/media/qr-runtime.ts index dcedf92f9cf..c3e2874fb8f 100644 --- a/src/media/qr-runtime.ts +++ b/src/media/qr-runtime.ts @@ -1,8 +1,22 @@ -let qrCodeTuiRuntimePromise: Promise | 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 | null = null; + +export async function loadQrCodeRuntime(): Promise { + 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; } diff --git a/src/media/qr-terminal.test.ts b/src/media/qr-terminal.test.ts new file mode 100644 index 00000000000..b4f43a591f5 --- /dev/null +++ b/src/media/qr-terminal.test.ts @@ -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()); + }); +}); diff --git a/src/media/qr-terminal.ts b/src/media/qr-terminal.ts index 3c214914508..c8ac6452ab5 100644 --- a/src/media/qr-terminal.ts +++ b/src/media/qr-terminal.ts @@ -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 { - 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", + }); } diff --git a/src/terminal/osc-progress.test.ts b/src/terminal/osc-progress.test.ts new file mode 100644 index 00000000000..537e75e369f --- /dev/null +++ b/src/terminal/osc-progress.test.ts @@ -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\\", + ]); + }); +}); diff --git a/src/terminal/osc-progress.ts b/src/terminal/osc-progress.ts new file mode 100644 index 00000000000..94b49c0346e --- /dev/null +++ b/src/terminal/osc-progress.ts @@ -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)); + }, + }; +} diff --git a/src/types/osc-progress.d.ts b/src/types/osc-progress.d.ts deleted file mode 100644 index cce56c26c36..00000000000 --- a/src/types/osc-progress.d.ts +++ /dev/null @@ -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; -} diff --git a/src/types/qrcode.d.ts b/src/types/qrcode.d.ts new file mode 100644 index 00000000000..9416e9718bf --- /dev/null +++ b/src/types/qrcode.d.ts @@ -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; + export function toDataURL(text: string, options?: QrCodeRenderOptions): Promise; + export function toFile( + filePath: string, + text: string, + options?: QrCodeRenderOptions, + ): Promise; + + const qrcode: { + toString: typeof toString; + toDataURL: typeof toDataURL; + toFile: typeof toFile; + }; + + export default qrcode; +}