From 1b0a8adb2b670db237dc45beb1940647a9aa3752 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 5 Dec 2025 14:51:06 +0900 Subject: [PATCH] refactor(comment-checker): remove WASM fallback, use CLI-only with lazy download - Remove tree-sitter-wasms and web-tree-sitter dependencies - Delete detector.ts (320 lines of WASM implementation) - Add downloader.ts for lazy binary download from GitHub Releases - Simplify index.ts to CLI-only mode - Cache binary at ~/.cache/oh-my-opencode/bin/ - Fall back to 'Comment checking disabled' when binary unavailable --- bun.lock | 11 +- package.json | 8 +- src/hooks/comment-checker/cli.ts | 124 ++++++++- src/hooks/comment-checker/constants.ts | 125 ---------- src/hooks/comment-checker/detector.ts | 319 ------------------------ src/hooks/comment-checker/downloader.ts | 210 ++++++++++++++++ src/hooks/comment-checker/index.ts | 99 +++----- src/hooks/comment-checker/types.ts | 6 - 8 files changed, 361 insertions(+), 541 deletions(-) delete mode 100644 src/hooks/comment-checker/detector.ts create mode 100644 src/hooks/comment-checker/downloader.ts diff --git a/bun.lock b/bun.lock index db67f64..9fde555 100644 --- a/bun.lock +++ b/bun.lock @@ -7,9 +7,8 @@ "dependencies": { "@ast-grep/cli": "^0.40.0", "@ast-grep/napi": "^0.40.0", + "@code-yeongyu/comment-checker": "^0.4.1", "@opencode-ai/plugin": "^1.0.7", - "tree-sitter-wasms": "^0.1.12", - "web-tree-sitter": "^0.24.7", "zod": "^4.1.8", }, "devDependencies": { @@ -23,6 +22,8 @@ }, "trustedDependencies": [ "@ast-grep/cli", + "@ast-grep/napi", + "@code-yeongyu/comment-checker", ], "packages": { "@ast-grep/cli": ["@ast-grep/cli@0.40.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@ast-grep/cli-darwin-arm64": "0.40.0", "@ast-grep/cli-darwin-x64": "0.40.0", "@ast-grep/cli-linux-arm64-gnu": "0.40.0", "@ast-grep/cli-linux-x64-gnu": "0.40.0", "@ast-grep/cli-win32-arm64-msvc": "0.40.0", "@ast-grep/cli-win32-ia32-msvc": "0.40.0", "@ast-grep/cli-win32-x64-msvc": "0.40.0" }, "bin": { "sg": "sg", "ast-grep": "ast-grep" } }, "sha512-L8AkflsfI2ZP70yIdrwqvjR02ScCuRmM/qNGnJWUkOFck+e6gafNVJ4e4jjGQlEul+dNdBpx36+O2Op629t47A=="], @@ -61,6 +62,8 @@ "@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Hk2IwfPqMFGZt5SRxsoWmGLxBXxprow4LRp1eG6V8EEiJCNHxZ9ZiEaIc5bNvMDBjHVSnqZAXT22dROhrcSKQg=="], + "@code-yeongyu/comment-checker": ["@code-yeongyu/comment-checker@0.4.1", "", { "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "comment-checker": "bin/comment-checker" } }, "sha512-E7p1V8CsRj9hMbwENd9BfxZGWYu+lKS5tXGuNNcNtkRMhWvwM/ononysKpLB7LXdxfSYAn0j7heJydyzEmm+lg=="], + "@opencode-ai/plugin": ["@opencode-ai/plugin@1.0.128", "", { "dependencies": { "@opencode-ai/sdk": "1.0.128", "zod": "4.1.8" } }, "sha512-M5vjz3I6KeoBSNduWmT5iHXRtTLCqICM5ocs+WrB3uxVorslcO3HVwcLzrERh/ntpxJ/1xhnHQaeG6Mg+P744A=="], "@opencode-ai/sdk": ["@opencode-ai/sdk@1.0.128", "", {}, "sha512-Kow3Ivg8bR8dNRp8C0LwF9e8+woIrwFgw3ZALycwCfqS/UujDkJiBeYHdr1l/07GSHP9sZPmvJ6POuvfZ923EA=="], @@ -95,14 +98,10 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "tree-sitter-wasms": ["tree-sitter-wasms@0.1.13", "", { "dependencies": { "tree-sitter-wasms": "^0.1.11" } }, "sha512-wT+cR6DwaIz80/vho3AvSF0N4txuNx/5bcRKoXouOfClpxh/qqrF4URNLQXbbt8MaAxeksZcZd1j8gcGjc+QxQ=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "web-tree-sitter": ["web-tree-sitter@0.24.7", "", {}, "sha512-CdC/TqVFbXqR+C51v38hv6wOPatKEUGxa39scAeFSm98wIhZxAYonhRQPSMmfZ2w7JDI0zQDdzdmgtNk06/krQ=="], - "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], } } diff --git a/package.json b/package.json index 4c82f6f..423b391 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oh-my-opencode", - "version": "0.1.13", + "version": "0.1.14", "description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -44,9 +44,8 @@ "dependencies": { "@ast-grep/cli": "^0.40.0", "@ast-grep/napi": "^0.40.0", + "@code-yeongyu/comment-checker": "^0.4.1", "@opencode-ai/plugin": "^1.0.7", - "tree-sitter-wasms": "^0.1.12", - "web-tree-sitter": "^0.24.7", "zod": "^4.1.8" }, "devDependencies": { @@ -58,6 +57,7 @@ }, "trustedDependencies": [ "@ast-grep/cli", - "@ast-grep/napi" + "@ast-grep/napi", + "@code-yeongyu/comment-checker" ] } diff --git a/src/hooks/comment-checker/cli.ts b/src/hooks/comment-checker/cli.ts index 307d666..c5b7bfa 100644 --- a/src/hooks/comment-checker/cli.ts +++ b/src/hooks/comment-checker/cli.ts @@ -3,6 +3,7 @@ import { createRequire } from "module" import { dirname, join } from "path" import { existsSync } from "fs" import * as fs from "fs" +import { getCachedBinaryPath, ensureCommentCheckerBinary } from "./downloader" const DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1" const DEBUG_FILE = "/tmp/comment-checker-debug.log" @@ -35,7 +36,12 @@ function getBinaryName(): string { return process.platform === "win32" ? "comment-checker.exe" : "comment-checker" } -function findCommentCheckerPath(): string | null { +/** + * Synchronously find comment-checker binary path. + * Checks installed packages, homebrew, cache, and system PATH. + * Does NOT trigger download. + */ +function findCommentCheckerPathSync(): string | null { const binaryName = getBinaryName() // 1. Try to find from @code-yeongyu/comment-checker package @@ -85,13 +91,87 @@ function findCommentCheckerPath(): string | null { } } - // 4. Try system PATH - const systemPath = "comment-checker" - debugLog("falling back to system PATH:", systemPath) - return systemPath + // 4. Try cached binary (lazy download location) + const cachedPath = getCachedBinaryPath() + if (cachedPath) { + debugLog("found binary in cache:", cachedPath) + return cachedPath + } + + // 5. Try system PATH (as fallback) + debugLog("no binary found in known locations") + return null } -export const COMMENT_CHECKER_CLI_PATH = findCommentCheckerPath() +// Cached resolved path +let resolvedCliPath: string | null = null +let initPromise: Promise | null = null + +/** + * Asynchronously get comment-checker binary path. + * Will trigger lazy download if binary not found. + */ +export async function getCommentCheckerPath(): Promise { + // Return cached path if already resolved + if (resolvedCliPath !== null) { + return resolvedCliPath + } + + // Return existing promise if initialization is in progress + if (initPromise) { + return initPromise + } + + initPromise = (async () => { + // First try sync path resolution + const syncPath = findCommentCheckerPathSync() + if (syncPath && existsSync(syncPath)) { + resolvedCliPath = syncPath + debugLog("using sync-resolved path:", syncPath) + return syncPath + } + + // Lazy download if not found + debugLog("triggering lazy download...") + const downloadedPath = await ensureCommentCheckerBinary() + if (downloadedPath) { + resolvedCliPath = downloadedPath + debugLog("using downloaded path:", downloadedPath) + return downloadedPath + } + + debugLog("no binary available") + return null + })() + + return initPromise +} + +/** + * Synchronously get comment-checker path (no download). + * Returns cached path or searches known locations. + */ +export function getCommentCheckerPathSync(): string | null { + return resolvedCliPath ?? findCommentCheckerPathSync() +} + +/** + * Start background initialization. + * Call this early to trigger download while other init happens. + */ +export function startBackgroundInit(): void { + if (!initPromise) { + initPromise = getCommentCheckerPath() + initPromise.then(path => { + debugLog("background init complete:", path || "no binary") + }).catch(err => { + debugLog("background init error:", err) + }) + } +} + +// Legacy export for backwards compatibility (sync, no download) +export const COMMENT_CHECKER_CLI_PATH = findCommentCheckerPathSync() export interface HookInput { session_id: string @@ -114,17 +194,29 @@ export interface CheckResult { message: string } -export async function runCommentChecker(input: HookInput): Promise { - if (!COMMENT_CHECKER_CLI_PATH) { +/** + * Run comment-checker CLI with given input. + * @param input Hook input to check + * @param cliPath Optional explicit path to CLI binary + */ +export async function runCommentChecker(input: HookInput, cliPath?: string): Promise { + const binaryPath = cliPath ?? resolvedCliPath ?? COMMENT_CHECKER_CLI_PATH + + if (!binaryPath) { debugLog("comment-checker binary not found") return { hasComments: false, message: "" } } + if (!existsSync(binaryPath)) { + debugLog("comment-checker binary does not exist:", binaryPath) + return { hasComments: false, message: "" } + } + const jsonInput = JSON.stringify(input) debugLog("running comment-checker with input:", jsonInput.substring(0, 200)) try { - const proc = spawn([COMMENT_CHECKER_CLI_PATH], { + const proc = spawn([binaryPath], { stdin: "pipe", stdout: "pipe", stderr: "pipe", @@ -158,6 +250,18 @@ export async function runCommentChecker(input: HookInput): Promise } } +/** + * Check if CLI is available (sync check, no download). + */ export function isCliAvailable(): boolean { - return COMMENT_CHECKER_CLI_PATH !== null && existsSync(COMMENT_CHECKER_CLI_PATH) + const path = getCommentCheckerPathSync() + return path !== null && existsSync(path) +} + +/** + * Check if CLI will be available (async, may trigger download). + */ +export async function ensureCliAvailable(): Promise { + const path = await getCommentCheckerPath() + return path !== null && existsSync(path) } diff --git a/src/hooks/comment-checker/constants.ts b/src/hooks/comment-checker/constants.ts index b51f51e..3732e31 100644 --- a/src/hooks/comment-checker/constants.ts +++ b/src/hooks/comment-checker/constants.ts @@ -1,117 +1,3 @@ -import type { LanguageConfig } from "./types" - -export const EXTENSION_TO_LANGUAGE: Record = { - py: "python", - js: "javascript", - jsx: "javascript", - ts: "typescript", - tsx: "tsx", - go: "golang", - java: "java", - kt: "kotlin", - scala: "scala", - c: "c", - h: "c", - cpp: "cpp", - cc: "cpp", - cxx: "cpp", - hpp: "cpp", - rs: "rust", - rb: "ruby", - sh: "bash", - bash: "bash", - cs: "csharp", - swift: "swift", - ex: "elixir", - exs: "elixir", - lua: "lua", - php: "php", - ml: "ocaml", - mli: "ocaml", - sql: "sql", - html: "html", - htm: "html", - css: "css", - yaml: "yaml", - yml: "yaml", - toml: "toml", - hcl: "hcl", - tf: "hcl", - dockerfile: "dockerfile", - proto: "protobuf", - svelte: "svelte", - elm: "elm", - groovy: "groovy", - cue: "cue", -} - -export const QUERY_TEMPLATES: Record = { - python: "(comment) @comment", - javascript: "(comment) @comment", - typescript: "(comment) @comment", - tsx: "(comment) @comment", - golang: "(comment) @comment", - rust: ` - (line_comment) @comment - (block_comment) @comment - `, - kotlin: ` - (line_comment) @comment - (multiline_comment) @comment - `, - java: ` - (line_comment) @comment - (block_comment) @comment - `, - c: "(comment) @comment", - cpp: "(comment) @comment", - csharp: "(comment) @comment", - ruby: "(comment) @comment", - bash: "(comment) @comment", - swift: "(comment) @comment", - elixir: "(comment) @comment", - lua: "(comment) @comment", - php: "(comment) @comment", - ocaml: "(comment) @comment", - sql: "(comment) @comment", - html: "(comment) @comment", - css: "(comment) @comment", - yaml: "(comment) @comment", - toml: "(comment) @comment", - hcl: "(comment) @comment", - dockerfile: "(comment) @comment", - protobuf: "(comment) @comment", - svelte: "(comment) @comment", - elm: "(comment) @comment", - groovy: "(comment) @comment", - cue: "(comment) @comment", - scala: "(comment) @comment", -} - -export const DOCSTRING_QUERIES: Record = { - python: ` - (module . (expression_statement (string) @docstring)) - (class_definition body: (block . (expression_statement (string) @docstring))) - (function_definition body: (block . (expression_statement (string) @docstring))) - `, - javascript: ` - (comment) @jsdoc - (#match? @jsdoc "^/\\\\*\\\\*") - `, - typescript: ` - (comment) @jsdoc - (#match? @jsdoc "^/\\\\*\\\\*") - `, - tsx: ` - (comment) @jsdoc - (#match? @jsdoc "^/\\\\*\\\\*") - `, - java: ` - (comment) @javadoc - (#match? @javadoc "^/\\\\*\\\\*") - `, -} - export const BDD_KEYWORDS = new Set([ "given", "when", @@ -191,14 +77,3 @@ Review in the above priority order and take the corresponding action EVERY TIME Detected comments/docstrings: ` - -export function getLanguageByExtension(filePath: string): string | null { - const lastDot = filePath.lastIndexOf(".") - if (lastDot === -1) { - const baseName = filePath.split("/").pop()?.toLowerCase() - if (baseName === "dockerfile") return "dockerfile" - return null - } - const ext = filePath.slice(lastDot + 1).toLowerCase() - return EXTENSION_TO_LANGUAGE[ext] ?? null -} diff --git a/src/hooks/comment-checker/detector.ts b/src/hooks/comment-checker/detector.ts deleted file mode 100644 index ecdb565..0000000 --- a/src/hooks/comment-checker/detector.ts +++ /dev/null @@ -1,319 +0,0 @@ -import type { CommentInfo, CommentType } from "./types" -import { getLanguageByExtension, QUERY_TEMPLATES, DOCSTRING_QUERIES } from "./constants" -import * as fs from "fs" - -// ============================================================================= -// Debug logging -// ============================================================================= - -const DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1" -const DEBUG_FILE = "/tmp/comment-checker-debug.log" - -function debugLog(...args: unknown[]) { - if (DEBUG) { - const msg = `[${new Date().toISOString()}] [comment-checker:detector] ${args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' ')}\n` - fs.appendFileSync(DEBUG_FILE, msg) - } -} - -// ============================================================================= -// Parser Manager (LSP-style background initialization) -// ============================================================================= - -interface ManagedLanguage { - language: unknown - initPromise?: Promise - isInitializing: boolean - lastUsedAt: number -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let parserClass: any = null -let parserInitPromise: Promise | null = null -const languageCache = new Map() - -const LANGUAGE_NAME_MAP: Record = { - golang: "go", - csharp: "c_sharp", - cpp: "cpp", -} - -const COMMON_LANGUAGES = [ - "python", - "typescript", - "javascript", - "tsx", - "go", - "rust", - "java", -] - -async function initParserClass(): Promise { - if (parserClass) return - - if (parserInitPromise) { - await parserInitPromise - return - } - - parserInitPromise = (async () => { - debugLog("importing web-tree-sitter...") - parserClass = (await import("web-tree-sitter")).default - - // Find wasm path relative to web-tree-sitter package at runtime - const webTreeSitterPath = import.meta.resolve("web-tree-sitter") - const packageDir = webTreeSitterPath.replace(/\/[^/]+$/, "").replace("file://", "") - const treeSitterWasmPath = `${packageDir}/tree-sitter.wasm` - debugLog("wasm path:", treeSitterWasmPath) - - await parserClass.init({ - locateFile: () => treeSitterWasmPath, - }) - debugLog("Parser class initialized") - })() - - await parserInitPromise -} - -async function getParser() { - await initParserClass() - return new parserClass() -} - -async function loadLanguageWasm(langName: string): Promise { - const mappedLang = LANGUAGE_NAME_MAP[langName] || langName - - try { - const wasmModule = await import(`tree-sitter-wasms/out/tree-sitter-${langName}.wasm`) - return wasmModule.default - } catch { - if (mappedLang !== langName) { - try { - const wasmModule = await import(`tree-sitter-wasms/out/tree-sitter-${mappedLang}.wasm`) - return wasmModule.default - } catch { - return null - } - } - return null - } -} - -async function getLanguage(langName: string): Promise { - const cached = languageCache.get(langName) - - if (cached) { - if (cached.initPromise) { - await cached.initPromise - } - cached.lastUsedAt = Date.now() - debugLog("using cached language:", langName) - return cached.language - } - - debugLog("loading language wasm:", langName) - - const initPromise = (async () => { - await initParserClass() - const wasmPath = await loadLanguageWasm(langName) - if (!wasmPath) { - debugLog("failed to load language wasm:", langName) - return null - } - return await parserClass!.Language.load(wasmPath) - })() - - languageCache.set(langName, { - language: null as unknown, - initPromise, - isInitializing: true, - lastUsedAt: Date.now(), - }) - - const language = await initPromise - const managed = languageCache.get(langName) - if (managed) { - managed.language = language - managed.initPromise = undefined - managed.isInitializing = false - } - - debugLog("language loaded and cached:", langName) - return language -} - -function warmupLanguage(langName: string): void { - if (languageCache.has(langName)) return - - debugLog("warming up language (background):", langName) - - const initPromise = (async () => { - await initParserClass() - const wasmPath = await loadLanguageWasm(langName) - if (!wasmPath) return null - return await parserClass!.Language.load(wasmPath) - })() - - languageCache.set(langName, { - language: null as unknown, - initPromise, - isInitializing: true, - lastUsedAt: Date.now(), - }) - - initPromise.then((language) => { - const managed = languageCache.get(langName) - if (managed) { - managed.language = language - managed.initPromise = undefined - managed.isInitializing = false - debugLog("warmup complete:", langName) - } - }).catch((err) => { - debugLog("warmup failed:", langName, err) - languageCache.delete(langName) - }) -} - -export function warmupCommonLanguages(): void { - debugLog("starting background warmup for common languages...") - initParserClass().then(() => { - for (const lang of COMMON_LANGUAGES) { - warmupLanguage(lang) - } - }).catch((err) => { - debugLog("warmup initialization failed:", err) - }) -} - -// ============================================================================= -// Public API -// ============================================================================= - -export function isSupportedFile(filePath: string): boolean { - return getLanguageByExtension(filePath) !== null -} - -function determineCommentType(text: string, nodeType: string): CommentType { - const stripped = text.trim() - - if (nodeType === "line_comment") { - return "line" - } - if (nodeType === "block_comment" || nodeType === "multiline_comment") { - return "block" - } - - if (stripped.startsWith('"""') || stripped.startsWith("'''")) { - return "docstring" - } - - if (stripped.startsWith("//") || stripped.startsWith("#")) { - return "line" - } - - if (stripped.startsWith("/*") || stripped.startsWith("