From 0c71e2cd5406de85e983b76ee45720dd9f4d9a1b Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 25 May 2026 23:13:16 -0400 Subject: [PATCH] refactor: replace Bun APIs with Node.js equivalents Replace Bun.argv with process.argv, Bun.file() with fs/promises, Bun.stringWidth with string-width package, and Bun.wrapAnsi with wrap-ansi package for better compatibility. --- bun.lock | 12 ++++++++++++ index.ts | 2 +- package.json | 4 +++- src/config/index.ts | 10 ++++++---- src/conversation/index.ts | 9 ++++++--- src/render/index.ts | 6 +++--- src/render/markdown.ts | 14 ++++++++------ 7 files changed, 39 insertions(+), 18 deletions(-) diff --git a/bun.lock b/bun.lock index 9468bf3..2c3a667 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,9 @@ "happy-dom-without-node": "^14.12.3", "marked": "^18.0.4", "shiki": "^4.0.2", + "string-width": "^8.2.1", "terminal-link": "^5.0.0", + "wrap-ansi": "^10.0.0", }, "devDependencies": { "@biomejs/biome": "2.4.12", @@ -101,6 +103,8 @@ "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], @@ -131,6 +135,8 @@ "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + "happy-dom-without-node": ["happy-dom-without-node@14.12.3", "", { "dependencies": { "entities": "^4.5.0", "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^14.0.0" } }, "sha512-1wp8+GFneT8mBjVnzancXHRuscEUH3vnb38lfCHPxuSu6OiRw1kQzHFbTGYlNCU2YXOlVIlzsS6xg9nAr7Xg6Q=="], "has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="], @@ -177,8 +183,12 @@ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "supports-hyperlinks": ["supports-hyperlinks@4.4.0", "", { "dependencies": { "has-flag": "^5.0.1", "supports-color": "^10.2.2" } }, "sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg=="], @@ -213,6 +223,8 @@ "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + "wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], diff --git a/index.ts b/index.ts index d8431c1..14530ef 100644 --- a/index.ts +++ b/index.ts @@ -7,7 +7,7 @@ import { Conversation } from "./src/conversation"; import Renderer from "./src/render"; const { values: args, positionals } = parseArgs({ - args: Bun.argv, + args: process.argv, options: { model: { type: "string" }, m: { type: "string" }, diff --git a/package.json b/package.json index 0ad14b9..bbca7b7 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,8 @@ "happy-dom-without-node": "^14.12.3", "marked": "^18.0.4", "shiki": "^4.0.2", - "terminal-link": "^5.0.0" + "string-width": "^8.2.1", + "terminal-link": "^5.0.0", + "wrap-ansi": "^10.0.0" } } diff --git a/src/config/index.ts b/src/config/index.ts index 8fed513..2ddabcc 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,9 +1,10 @@ +import fs from "node:fs/promises"; import os from "node:os"; import { autocomplete, intro, isCancel, outro, text } from "@clack/prompts"; import chalk from "chalk"; const home = os.homedir(); -const configFile = Bun.file(`${home}/.lmc_config.json`); +const configPath = `${home}/.lmc_config.json`; export interface AppConfig { baseURL: string; @@ -36,7 +37,7 @@ export async function initConfig(old: AppConfig | null): Promise { const config = { baseURL, model } satisfies AppConfig; - await configFile.write(JSON.stringify(config, null, " ")); + await fs.writeFile(configPath, JSON.stringify(config, null, " ")); outro(chalk.green("Configuration saved!")); @@ -63,10 +64,11 @@ export async function loadConfig( ): Promise { let config: AppConfig; try { - config = await configFile.json(); + const text = await fs.readFile(configPath); + config = JSON.parse(text.toString("utf8")); } catch { if (configFlag) return null; - console.error(`Config not found at ${configFile.name}`); + console.error(`Config not found at ${configPath}`); return null; } diff --git a/src/conversation/index.ts b/src/conversation/index.ts index 9fb01f4..6d408c2 100644 --- a/src/conversation/index.ts +++ b/src/conversation/index.ts @@ -1,8 +1,9 @@ +import fs from "node:fs/promises"; import os from "node:os"; import type { ModelMessage } from "ai"; const home = os.homedir(); -const convoFile = Bun.file(`${home}/.lmc_convo.json`); +const convoPath = `${home}/.lmc_convo.json`; export interface IConversation { model: string; @@ -16,7 +17,8 @@ export class Conversation implements IConversation { async load() { try { - const convo: IConversation = await convoFile.json(); + const text = await fs.readFile(convoPath); + const convo: IConversation = JSON.parse(text.toString("utf8")); this.model = convo.model; this.messages = convo.messages; } catch { @@ -25,7 +27,8 @@ export class Conversation implements IConversation { } async save() { - await convoFile.write( + await fs.writeFile( + convoPath, JSON.stringify({ model: this.model, messages: this.messages, diff --git a/src/render/index.ts b/src/render/index.ts index 91ac9f4..01fce63 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -8,6 +8,7 @@ import type { } from "ai"; import styles from "ansi-styles"; import { chalkStderr, supportsColorStderr } from "chalk"; +import stringWidth from "string-width"; import { renderMarkdown } from "./markdown"; export default class Renderer { @@ -40,12 +41,11 @@ export default class Renderer { private checkContainment(text: string, pipe: WriteStream = process.stdout) { const [columns, rows] = pipe.getWindowSize(); - this.charsSinceNL += Bun.stringWidth(text); + this.charsSinceNL += stringWidth(text); if (text.includes("\n")) { this.lineCount += text.match(/\n/g)?.length ?? 1; this.charsSinceNL = - Bun.stringWidth(text) - - Bun.stringWidth(text.slice(0, text.lastIndexOf("\n"))); + stringWidth(text) - stringWidth(text.slice(0, text.lastIndexOf("\n"))); } else if (this.charsSinceNL > columns) { this.lineCount++; this.charsSinceNL = this.charsSinceNL - columns; diff --git a/src/render/markdown.ts b/src/render/markdown.ts index dcd85af..a745b82 100644 --- a/src/render/markdown.ts +++ b/src/render/markdown.ts @@ -7,7 +7,9 @@ import { createHighlighter, type HighlighterGeneric, } from "shiki"; +import stringWidth from "string-width"; import terminalLink from "terminal-link"; +import wrapAnsi from "wrap-ansi"; import { codeToANSI } from "./codeToANSI"; let highlighter: HighlighterGeneric | undefined; @@ -15,7 +17,7 @@ let highligherLangs = ""; class SWCell extends Cell { public override get length() { - return Bun.stringWidth(this.toString(), { ambiguousIsNarrow: true }); + return stringWidth(this.toString(), { ambiguousIsNarrow: true }); } } @@ -23,7 +25,7 @@ class LmcRenderer extends Renderer { private columns: number; constructor(columns: number) { - super(); + super({ gfm: true }); this.columns = columns; } @@ -38,7 +40,7 @@ class LmcRenderer extends Renderer { ]; const color = colors[depth - 1] ?? chalk.white; const text = this.parser.parseInline(tokens); - return Bun.wrapAnsi( + return wrapAnsi( color.bold(`${"#".repeat(depth)} ${text}\n\n`), this.columns, { trim: false }, @@ -47,12 +49,12 @@ class LmcRenderer extends Renderer { override paragraph({ tokens }: Tokens.Paragraph) { const text = this.parser.parseInline(tokens); - return Bun.wrapAnsi(`${text}\n\n`, this.columns, { trim: false }); + return wrapAnsi(`${text}\n\n`, this.columns, { trim: false }); } override blockquote({ tokens }: Tokens.Blockquote) { const text = this.parser.parse(tokens).trim(); - const lines = Bun.wrapAnsi(text, this.columns - 2, { trim: false }) + const lines = wrapAnsi(text, this.columns - 2, { trim: false }) .replace(/\n+/g, "\n") .concat("\n") .split("\n"); @@ -96,7 +98,7 @@ class LmcRenderer extends Renderer { } const cols = this.columns - 2; - return Bun.wrapAnsi(`${marker} ${text}`, cols, { trim: false }) + return wrapAnsi(`${marker} ${text}`, cols, { trim: false }) .split("\n") .map((line) => (line.length > 0 ? ` ${line}` : "")) .join("\n")