From 4528aa5cf0a1423cf6dbfaf0bf83b7745ee4f3df Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 14:12:51 +0000 Subject: [PATCH] refactor(cli): flatten Encoding/EncodedIO namespaces to module exports Replace `export namespace Encoding`/`EncodedIO` wrappers with top-level exports and switch consumers to `import * as Encoding`/`EncodedIO`. The module itself acts as the namespace, avoiding the TS-namespace wrapper that tree-shakers and some module resolvers handle awkwardly. --- packages/opencode/src/kilocode/encoding.ts | 250 +++++++++--------- .../opencode/src/kilocode/tool/encoded-io.ts | 16 +- packages/opencode/src/patch/index.ts | 2 +- packages/opencode/src/tool/apply_patch.ts | 2 +- packages/opencode/src/tool/edit.ts | 2 +- packages/opencode/src/tool/read.ts | 2 +- packages/opencode/src/tool/write.ts | 2 +- .../opencode/test/kilocode/encoding.test.ts | 2 +- 8 files changed, 140 insertions(+), 138 deletions(-) diff --git a/packages/opencode/src/kilocode/encoding.ts b/packages/opencode/src/kilocode/encoding.ts index 314384fa079..1073ac8bfe2 100644 --- a/packages/opencode/src/kilocode/encoding.ts +++ b/packages/opencode/src/kilocode/encoding.ts @@ -24,134 +24,134 @@ import iconv from "iconv-lite" * * iconv-lite's UTF codecs strip BOMs on decode and do not emit them on encode, * so UTF BOMs are handled explicitly in {@link encode} to round-trip cleanly. + * + * Consumers should import this module as a namespace: + * import * as Encoding from "../kilocode/encoding" */ -export namespace Encoding { - export const DEFAULT = "utf-8" - /** - * Synthetic label for UTF-8 files that start with a BOM. iconv-lite's utf-8 - * codec always strips BOMs on decode and never emits one on encode, so we - * track the "with BOM" case explicitly to round-trip it faithfully. - */ - export const UTF8_BOM = "utf-8-bom" - const UTF8_BOM_BYTES = Buffer.from([0xef, 0xbb, 0xbf]) - function hasUtf8Bom(bytes: Buffer): boolean { - return bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf +export const DEFAULT = "utf-8" +/** + * Synthetic label for UTF-8 files that start with a BOM. iconv-lite's utf-8 + * codec always strips BOMs on decode and never emits one on encode, so we + * track the "with BOM" case explicitly to round-trip it faithfully. + */ +export const UTF8_BOM = "utf-8-bom" +const UTF8_BOM_BYTES = Buffer.from([0xef, 0xbb, 0xbf]) + +function hasUtf8Bom(bytes: Buffer): boolean { + return bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf +} + +/** True if `bytes[0..limit]` starts with a UTF-16 LE or BE byte-order mark. */ +export function hasUtf16Bom(bytes: Buffer, limit = bytes.length): boolean { + if (limit < 2) return false + // UTF-32 LE starts with FF FE 00 00, so exclude that to avoid misclassifying it as UTF-16 LE. + if (hasUtf32Bom(bytes, limit)) return false + return (bytes[0] === 0xff && bytes[1] === 0xfe) || (bytes[0] === 0xfe && bytes[1] === 0xff) +} + +/** True if `bytes[0..limit]` starts with a UTF-32 LE or BE byte-order mark. */ +export function hasUtf32Bom(bytes: Buffer, limit = bytes.length): boolean { + if (limit < 4) return false + const le = bytes[0] === 0xff && bytes[1] === 0xfe && bytes[2] === 0x00 && bytes[3] === 0x00 + const be = bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0xfe && bytes[3] === 0xff + return le || be +} + +/** Remap jschardet labels to iconv-lite compatible names. */ +function normalize(name: string): string { + const lower = name.toLowerCase().replace(/[^a-z0-9]/g, "") + const map: Record = { + utf8: "utf-8", + utf16le: "utf-16le", + utf16be: "utf-16be", + utf32le: "utf-32le", + utf32be: "utf-32be", + ascii: "utf-8", + iso88591: "iso-8859-1", + iso88592: "iso-8859-2", + iso88595: "iso-8859-5", + iso88597: "iso-8859-7", + iso88598: "iso-8859-8", + iso88599: "iso-8859-9", + windows1250: "windows-1250", + windows1251: "windows-1251", + windows1252: "windows-1252", + windows1253: "windows-1253", + windows1255: "windows-1255", + shiftjis: "Shift_JIS", + eucjp: "euc-jp", + iso2022jp: "iso-2022-jp", + euckr: "euc-kr", + iso2022kr: "iso-2022-kr", + big5: "big5", + gb2312: "gb2312", + gb18030: "gb18030", + koi8r: "koi8-r", + maccyrillic: "x-mac-cyrillic", + ibm855: "cp855", + ibm866: "cp866", + tis620: "tis-620", } + return map[lower] ?? name +} - /** True if `bytes[0..limit]` starts with a UTF-16 LE or BE byte-order mark. */ - export function hasUtf16Bom(bytes: Buffer, limit = bytes.length): boolean { - if (limit < 2) return false - // UTF-32 LE starts with FF FE 00 00, so exclude that to avoid misclassifying it as UTF-16 LE. - if (hasUtf32Bom(bytes, limit)) return false - return (bytes[0] === 0xff && bytes[1] === 0xfe) || (bytes[0] === 0xfe && bytes[1] === 0xff) - } - - /** True if `bytes[0..limit]` starts with a UTF-32 LE or BE byte-order mark. */ - export function hasUtf32Bom(bytes: Buffer, limit = bytes.length): boolean { - if (limit < 4) return false - const le = bytes[0] === 0xff && bytes[1] === 0xfe && bytes[2] === 0x00 && bytes[3] === 0x00 - const be = bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0xfe && bytes[3] === 0xff - return le || be - } - - /** Remap jschardet labels to iconv-lite compatible names. */ - function normalize(name: string): string { - const lower = name.toLowerCase().replace(/[^a-z0-9]/g, "") - const map: Record = { - utf8: "utf-8", - utf16le: "utf-16le", - utf16be: "utf-16be", - utf32le: "utf-32le", - utf32be: "utf-32be", - ascii: "utf-8", - iso88591: "iso-8859-1", - iso88592: "iso-8859-2", - iso88595: "iso-8859-5", - iso88597: "iso-8859-7", - iso88598: "iso-8859-8", - iso88599: "iso-8859-9", - windows1250: "windows-1250", - windows1251: "windows-1251", - windows1252: "windows-1252", - windows1253: "windows-1253", - windows1255: "windows-1255", - shiftjis: "Shift_JIS", - eucjp: "euc-jp", - iso2022jp: "iso-2022-jp", - euckr: "euc-kr", - iso2022kr: "iso-2022-kr", - big5: "big5", - gb2312: "gb2312", - gb18030: "gb18030", - koi8r: "koi8-r", - maccyrillic: "x-mac-cyrillic", - ibm855: "cp855", - ibm866: "cp866", - tis620: "tis-620", - } - return map[lower] ?? name - } - - function isUtf8(bytes: Buffer): boolean { - try { - new TextDecoder("utf-8", { fatal: true }).decode(bytes) - return true - } catch { - return false - } - } - - export function detect(bytes: Buffer): string { - if (bytes.length === 0) return DEFAULT - if (isUtf8(bytes)) return hasUtf8Bom(bytes) ? UTF8_BOM : DEFAULT - const result = jschardet.detect(bytes) - if (!result.encoding) return DEFAULT - const enc = normalize(result.encoding) - if (!iconv.encodingExists(enc)) return DEFAULT - return enc - } - - export function decode(bytes: Buffer, encoding: string): string { - if (encoding === UTF8_BOM) return iconv.decode(bytes, "utf-8") - return iconv.decode(bytes, encoding) - } - - export function encode(text: string, encoding: string): Buffer { - // iconv-lite's UTF codecs strip/ignore BOMs, but we support "UTF-X with BOM" - // as a distinct variant. Prepend the BOM manually so round-tripping keeps - // the original byte signature intact. Strip a leading U+FEFF from `text` - // first so we never emit a double BOM when the decoded text already - // contains one (e.g. if a tool round-trips content verbatim). - const body = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text - if (encoding === UTF8_BOM) return Buffer.concat([UTF8_BOM_BYTES, iconv.encode(body, "utf-8")]) - const lower = encoding.toLowerCase() - if (lower === "utf-16le") return Buffer.concat([Buffer.from([0xff, 0xfe]), iconv.encode(body, encoding)]) - if (lower === "utf-16be") return Buffer.concat([Buffer.from([0xfe, 0xff]), iconv.encode(body, encoding)]) - if (lower === "utf-32le") - return Buffer.concat([Buffer.from([0xff, 0xfe, 0x00, 0x00]), iconv.encode(body, encoding)]) - if (lower === "utf-32be") - return Buffer.concat([Buffer.from([0x00, 0x00, 0xfe, 0xff]), iconv.encode(body, encoding)]) - return iconv.encode(text, encoding) - } - - /** Read a file, detecting its encoding. */ - export async function read(path: string): Promise<{ text: string; encoding: string }> { - const bytes = await readFile(path) - const encoding = detect(bytes) - return { text: decode(bytes, encoding), encoding } - } - - /** Synchronous read, detecting encoding. */ - export function readSync(path: string): { text: string; encoding: string } { - const bytes = readFileSync(path) - const encoding = detect(bytes) - return { text: decode(bytes, encoding), encoding } - } - - /** Write text, ensuring parent directory exists, using the given encoding. */ - export async function write(path: string, text: string, encoding: string = DEFAULT): Promise { - await mkdir(dirname(path), { recursive: true }) - await writeFile(path, encode(text, encoding)) +function isUtf8(bytes: Buffer): boolean { + try { + new TextDecoder("utf-8", { fatal: true }).decode(bytes) + return true + } catch { + return false } } + +export function detect(bytes: Buffer): string { + if (bytes.length === 0) return DEFAULT + if (isUtf8(bytes)) return hasUtf8Bom(bytes) ? UTF8_BOM : DEFAULT + const result = jschardet.detect(bytes) + if (!result.encoding) return DEFAULT + const enc = normalize(result.encoding) + if (!iconv.encodingExists(enc)) return DEFAULT + return enc +} + +export function decode(bytes: Buffer, encoding: string): string { + if (encoding === UTF8_BOM) return iconv.decode(bytes, "utf-8") + return iconv.decode(bytes, encoding) +} + +export function encode(text: string, encoding: string): Buffer { + // iconv-lite's UTF codecs strip/ignore BOMs, but we support "UTF-X with BOM" + // as a distinct variant. Prepend the BOM manually so round-tripping keeps + // the original byte signature intact. Strip a leading U+FEFF from `text` + // first so we never emit a double BOM when the decoded text already + // contains one (e.g. if a tool round-trips content verbatim). + const body = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text + if (encoding === UTF8_BOM) return Buffer.concat([UTF8_BOM_BYTES, iconv.encode(body, "utf-8")]) + const lower = encoding.toLowerCase() + if (lower === "utf-16le") return Buffer.concat([Buffer.from([0xff, 0xfe]), iconv.encode(body, encoding)]) + if (lower === "utf-16be") return Buffer.concat([Buffer.from([0xfe, 0xff]), iconv.encode(body, encoding)]) + if (lower === "utf-32le") return Buffer.concat([Buffer.from([0xff, 0xfe, 0x00, 0x00]), iconv.encode(body, encoding)]) + if (lower === "utf-32be") return Buffer.concat([Buffer.from([0x00, 0x00, 0xfe, 0xff]), iconv.encode(body, encoding)]) + return iconv.encode(text, encoding) +} + +/** Read a file, detecting its encoding. */ +export async function read(path: string): Promise<{ text: string; encoding: string }> { + const bytes = await readFile(path) + const encoding = detect(bytes) + return { text: decode(bytes, encoding), encoding } +} + +/** Synchronous read, detecting encoding. */ +export function readSync(path: string): { text: string; encoding: string } { + const bytes = readFileSync(path) + const encoding = detect(bytes) + return { text: decode(bytes, encoding), encoding } +} + +/** Write text, ensuring parent directory exists, using the given encoding. */ +export async function write(path: string, text: string, encoding: string = DEFAULT): Promise { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, encode(text, encoding)) +} diff --git a/packages/opencode/src/kilocode/tool/encoded-io.ts b/packages/opencode/src/kilocode/tool/encoded-io.ts index 19c74b4e4bc..d06ec30f90d 100644 --- a/packages/opencode/src/kilocode/tool/encoded-io.ts +++ b/packages/opencode/src/kilocode/tool/encoded-io.ts @@ -1,17 +1,19 @@ import { Effect } from "effect" -import { Encoding } from "../encoding" +import * as Encoding from "../encoding" /** * Effect wrappers around {@link Encoding.read} and {@link Encoding.write} so * tool code can preserve file encoding without leaking Node/async boilerplate * into each call site. Uses {@link Effect.tryPromise} so I/O failures surface * as typed errors that can be recovered with `.pipe(Effect.catch(...))`. + * + * Consumers should import this module as a namespace: + * import * as EncodedIO from "../kilocode/tool/encoded-io" */ -export namespace EncodedIO { - const wrap = (cause: unknown) => (cause instanceof Error ? cause : new Error(String(cause))) - export const read = (path: string) => Effect.tryPromise({ try: () => Encoding.read(path), catch: wrap }) +const wrap = (cause: unknown) => (cause instanceof Error ? cause : new Error(String(cause))) - export const write = (path: string, text: string, encoding: string = Encoding.DEFAULT) => - Effect.tryPromise({ try: () => Encoding.write(path, text, encoding), catch: wrap }) -} +export const read = (path: string) => Effect.tryPromise({ try: () => Encoding.read(path), catch: wrap }) + +export const write = (path: string, text: string, encoding: string = Encoding.DEFAULT) => + Effect.tryPromise({ try: () => Encoding.write(path, text, encoding), catch: wrap }) diff --git a/packages/opencode/src/patch/index.ts b/packages/opencode/src/patch/index.ts index eeec1731961..b7d0989bb2b 100644 --- a/packages/opencode/src/patch/index.ts +++ b/packages/opencode/src/patch/index.ts @@ -2,7 +2,7 @@ import z from "zod" import * as path from "path" import * as fs from "fs/promises" import * as Log from "@opencode-ai/core/util/log" -import { Encoding } from "../kilocode/encoding" // kilocode_change +import * as Encoding from "../kilocode/encoding" // kilocode_change import * as Bom from "../util/bom" const log = Log.create({ service: "patch" }) diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index 908f0a5e012..186272d9680 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -14,7 +14,7 @@ import DESCRIPTION from "./apply_patch.txt" import { File } from "../file" import { filterDiagnostics } from "./diagnostics" // kilocode_change import { ConfigValidation } from "../kilocode/config-validation" // kilocode_change -import { EncodedIO } from "../kilocode/tool/encoded-io" // kilocode_change +import * as EncodedIO from "../kilocode/tool/encoded-io" // kilocode_change import { Format } from "../format" import * as Bom from "@/util/bom" diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 0d1defe83bf..92ccff2c927 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -20,7 +20,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem" import * as Bom from "@/util/bom" import { filterDiagnostics } from "./diagnostics" // kilocode_change import { ConfigValidation } from "../kilocode/config-validation" // kilocode_change -import { EncodedIO } from "../kilocode/tool/encoded-io" // kilocode_change +import * as EncodedIO from "../kilocode/tool/encoded-io" // kilocode_change const MAX_DIFF_CONTENT = 500_000 // kilocode_change diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index a02ab17fc00..14490791462 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -13,7 +13,7 @@ import { assertExternalDirectoryEffect } from "./external-directory" import { Instruction } from "../session/instruction" import { isImageAttachment, isPdfAttachment, sniffAttachmentMime } from "@/util/media" // kilocode_change start -import { Encoding } from "../kilocode/encoding" +import * as Encoding from "../kilocode/encoding" // kilocode_change end const DEFAULT_READ_LIMIT = 2000 diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 245c1aef54c..59575bfa718 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -15,7 +15,7 @@ import { trimDiff, buildFileDiff } from "./edit" // kilocode_change import { assertExternalDirectoryEffect } from "./external-directory" import { filterDiagnostics } from "./diagnostics" // kilocode_change import { ConfigValidation } from "../kilocode/config-validation" // kilocode_change -import { EncodedIO } from "../kilocode/tool/encoded-io" // kilocode_change +import * as EncodedIO from "../kilocode/tool/encoded-io" // kilocode_change import * as Bom from "@/util/bom" const MAX_PROJECT_DIAGNOSTICS_FILES = 5 diff --git a/packages/opencode/test/kilocode/encoding.test.ts b/packages/opencode/test/kilocode/encoding.test.ts index 177600f0cfb..ff8fbe1086d 100644 --- a/packages/opencode/test/kilocode/encoding.test.ts +++ b/packages/opencode/test/kilocode/encoding.test.ts @@ -11,7 +11,7 @@ import fs from "fs/promises" import os from "os" import path from "path" import iconv from "iconv-lite" -import { Encoding } from "../../src/kilocode/encoding" +import * as Encoding from "../../src/kilocode/encoding" const BOM = { utf8: Buffer.from([0xef, 0xbb, 0xbf]),