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.
This commit is contained in:
mitchell 2026-05-25 23:13:16 -04:00
parent b212fee200
commit 0c71e2cd54
7 changed files with 39 additions and 18 deletions

View file

@ -15,7 +15,9 @@
"happy-dom-without-node": "^14.12.3", "happy-dom-without-node": "^14.12.3",
"marked": "^18.0.4", "marked": "^18.0.4",
"shiki": "^4.0.2", "shiki": "^4.0.2",
"string-width": "^8.2.1",
"terminal-link": "^5.0.0", "terminal-link": "^5.0.0",
"wrap-ansi": "^10.0.0",
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.4.12", "@biomejs/biome": "2.4.12",
@ -101,6 +103,8 @@
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], "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=="], "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],

View file

@ -7,7 +7,7 @@ import { Conversation } from "./src/conversation";
import Renderer from "./src/render"; import Renderer from "./src/render";
const { values: args, positionals } = parseArgs({ const { values: args, positionals } = parseArgs({
args: Bun.argv, args: process.argv,
options: { options: {
model: { type: "string" }, model: { type: "string" },
m: { type: "string" }, m: { type: "string" },

View file

@ -37,6 +37,8 @@
"happy-dom-without-node": "^14.12.3", "happy-dom-without-node": "^14.12.3",
"marked": "^18.0.4", "marked": "^18.0.4",
"shiki": "^4.0.2", "shiki": "^4.0.2",
"terminal-link": "^5.0.0" "string-width": "^8.2.1",
"terminal-link": "^5.0.0",
"wrap-ansi": "^10.0.0"
} }
} }

View file

@ -1,9 +1,10 @@
import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import { autocomplete, intro, isCancel, outro, text } from "@clack/prompts"; import { autocomplete, intro, isCancel, outro, text } from "@clack/prompts";
import chalk from "chalk"; import chalk from "chalk";
const home = os.homedir(); const home = os.homedir();
const configFile = Bun.file(`${home}/.lmc_config.json`); const configPath = `${home}/.lmc_config.json`;
export interface AppConfig { export interface AppConfig {
baseURL: string; baseURL: string;
@ -36,7 +37,7 @@ export async function initConfig(old: AppConfig | null): Promise<AppConfig> {
const config = { baseURL, model } satisfies AppConfig; 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!")); outro(chalk.green("Configuration saved!"));
@ -63,10 +64,11 @@ export async function loadConfig(
): Promise<AppConfig | null> { ): Promise<AppConfig | null> {
let config: AppConfig; let config: AppConfig;
try { try {
config = await configFile.json(); const text = await fs.readFile(configPath);
config = JSON.parse(text.toString("utf8"));
} catch { } catch {
if (configFlag) return null; if (configFlag) return null;
console.error(`Config not found at ${configFile.name}`); console.error(`Config not found at ${configPath}`);
return null; return null;
} }

View file

@ -1,8 +1,9 @@
import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import type { ModelMessage } from "ai"; import type { ModelMessage } from "ai";
const home = os.homedir(); const home = os.homedir();
const convoFile = Bun.file(`${home}/.lmc_convo.json`); const convoPath = `${home}/.lmc_convo.json`;
export interface IConversation { export interface IConversation {
model: string; model: string;
@ -16,7 +17,8 @@ export class Conversation implements IConversation {
async load() { async load() {
try { 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.model = convo.model;
this.messages = convo.messages; this.messages = convo.messages;
} catch { } catch {
@ -25,7 +27,8 @@ export class Conversation implements IConversation {
} }
async save() { async save() {
await convoFile.write( await fs.writeFile(
convoPath,
JSON.stringify({ JSON.stringify({
model: this.model, model: this.model,
messages: this.messages, messages: this.messages,

View file

@ -8,6 +8,7 @@ import type {
} from "ai"; } from "ai";
import styles from "ansi-styles"; import styles from "ansi-styles";
import { chalkStderr, supportsColorStderr } from "chalk"; import { chalkStderr, supportsColorStderr } from "chalk";
import stringWidth from "string-width";
import { renderMarkdown } from "./markdown"; import { renderMarkdown } from "./markdown";
export default class Renderer { export default class Renderer {
@ -40,12 +41,11 @@ export default class Renderer {
private checkContainment(text: string, pipe: WriteStream = process.stdout) { private checkContainment(text: string, pipe: WriteStream = process.stdout) {
const [columns, rows] = pipe.getWindowSize(); const [columns, rows] = pipe.getWindowSize();
this.charsSinceNL += Bun.stringWidth(text); this.charsSinceNL += stringWidth(text);
if (text.includes("\n")) { if (text.includes("\n")) {
this.lineCount += text.match(/\n/g)?.length ?? 1; this.lineCount += text.match(/\n/g)?.length ?? 1;
this.charsSinceNL = this.charsSinceNL =
Bun.stringWidth(text) - stringWidth(text) - stringWidth(text.slice(0, text.lastIndexOf("\n")));
Bun.stringWidth(text.slice(0, text.lastIndexOf("\n")));
} else if (this.charsSinceNL > columns) { } else if (this.charsSinceNL > columns) {
this.lineCount++; this.lineCount++;
this.charsSinceNL = this.charsSinceNL - columns; this.charsSinceNL = this.charsSinceNL - columns;

View file

@ -7,7 +7,9 @@ import {
createHighlighter, createHighlighter,
type HighlighterGeneric, type HighlighterGeneric,
} from "shiki"; } from "shiki";
import stringWidth from "string-width";
import terminalLink from "terminal-link"; import terminalLink from "terminal-link";
import wrapAnsi from "wrap-ansi";
import { codeToANSI } from "./codeToANSI"; import { codeToANSI } from "./codeToANSI";
let highlighter: HighlighterGeneric<BundledLanguage, BundledTheme> | undefined; let highlighter: HighlighterGeneric<BundledLanguage, BundledTheme> | undefined;
@ -15,7 +17,7 @@ let highligherLangs = "";
class SWCell extends Cell { class SWCell extends Cell {
public override get length() { 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; private columns: number;
constructor(columns: number) { constructor(columns: number) {
super(); super({ gfm: true });
this.columns = columns; this.columns = columns;
} }
@ -38,7 +40,7 @@ class LmcRenderer extends Renderer {
]; ];
const color = colors[depth - 1] ?? chalk.white; const color = colors[depth - 1] ?? chalk.white;
const text = this.parser.parseInline(tokens); const text = this.parser.parseInline(tokens);
return Bun.wrapAnsi( return wrapAnsi(
color.bold(`${"#".repeat(depth)} ${text}\n\n`), color.bold(`${"#".repeat(depth)} ${text}\n\n`),
this.columns, this.columns,
{ trim: false }, { trim: false },
@ -47,12 +49,12 @@ class LmcRenderer extends Renderer {
override paragraph({ tokens }: Tokens.Paragraph) { override paragraph({ tokens }: Tokens.Paragraph) {
const text = this.parser.parseInline(tokens); 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) { override blockquote({ tokens }: Tokens.Blockquote) {
const text = this.parser.parse(tokens).trim(); 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") .replace(/\n+/g, "\n")
.concat("\n") .concat("\n")
.split("\n"); .split("\n");
@ -96,7 +98,7 @@ class LmcRenderer extends Renderer {
} }
const cols = this.columns - 2; const cols = this.columns - 2;
return Bun.wrapAnsi(`${marker} ${text}`, cols, { trim: false }) return wrapAnsi(`${marker} ${text}`, cols, { trim: false })
.split("\n") .split("\n")
.map((line) => (line.length > 0 ? ` ${line}` : "")) .map((line) => (line.length > 0 ? ` ${line}` : ""))
.join("\n") .join("\n")