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
This commit is contained in:
11
bun.lock
11
bun.lock
@@ -7,9 +7,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ast-grep/cli": "^0.40.0",
|
"@ast-grep/cli": "^0.40.0",
|
||||||
"@ast-grep/napi": "^0.40.0",
|
"@ast-grep/napi": "^0.40.0",
|
||||||
|
"@code-yeongyu/comment-checker": "^0.4.1",
|
||||||
"@opencode-ai/plugin": "^1.0.7",
|
"@opencode-ai/plugin": "^1.0.7",
|
||||||
"tree-sitter-wasms": "^0.1.12",
|
|
||||||
"web-tree-sitter": "^0.24.7",
|
|
||||||
"zod": "^4.1.8",
|
"zod": "^4.1.8",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -23,6 +22,8 @@
|
|||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"@ast-grep/cli",
|
"@ast-grep/cli",
|
||||||
|
"@ast-grep/napi",
|
||||||
|
"@code-yeongyu/comment-checker",
|
||||||
],
|
],
|
||||||
"packages": {
|
"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=="],
|
"@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=="],
|
"@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/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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oh-my-opencode",
|
"name": "oh-my-opencode",
|
||||||
"version": "0.1.13",
|
"version": "0.1.14",
|
||||||
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
|
"description": "OpenCode plugin - custom agents (oracle, librarian) and enhanced features",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
@@ -44,9 +44,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ast-grep/cli": "^0.40.0",
|
"@ast-grep/cli": "^0.40.0",
|
||||||
"@ast-grep/napi": "^0.40.0",
|
"@ast-grep/napi": "^0.40.0",
|
||||||
|
"@code-yeongyu/comment-checker": "^0.4.1",
|
||||||
"@opencode-ai/plugin": "^1.0.7",
|
"@opencode-ai/plugin": "^1.0.7",
|
||||||
"tree-sitter-wasms": "^0.1.12",
|
|
||||||
"web-tree-sitter": "^0.24.7",
|
|
||||||
"zod": "^4.1.8"
|
"zod": "^4.1.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -58,6 +57,7 @@
|
|||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"@ast-grep/cli",
|
"@ast-grep/cli",
|
||||||
"@ast-grep/napi"
|
"@ast-grep/napi",
|
||||||
|
"@code-yeongyu/comment-checker"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createRequire } from "module"
|
|||||||
import { dirname, join } from "path"
|
import { dirname, join } from "path"
|
||||||
import { existsSync } from "fs"
|
import { existsSync } from "fs"
|
||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
|
import { getCachedBinaryPath, ensureCommentCheckerBinary } from "./downloader"
|
||||||
|
|
||||||
const DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1"
|
const DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1"
|
||||||
const DEBUG_FILE = "/tmp/comment-checker-debug.log"
|
const DEBUG_FILE = "/tmp/comment-checker-debug.log"
|
||||||
@@ -35,7 +36,12 @@ function getBinaryName(): string {
|
|||||||
return process.platform === "win32" ? "comment-checker.exe" : "comment-checker"
|
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()
|
const binaryName = getBinaryName()
|
||||||
|
|
||||||
// 1. Try to find from @code-yeongyu/comment-checker package
|
// 1. Try to find from @code-yeongyu/comment-checker package
|
||||||
@@ -85,13 +91,87 @@ function findCommentCheckerPath(): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Try system PATH
|
// 4. Try cached binary (lazy download location)
|
||||||
const systemPath = "comment-checker"
|
const cachedPath = getCachedBinaryPath()
|
||||||
debugLog("falling back to system PATH:", systemPath)
|
if (cachedPath) {
|
||||||
return systemPath
|
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<string | null> | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously get comment-checker binary path.
|
||||||
|
* Will trigger lazy download if binary not found.
|
||||||
|
*/
|
||||||
|
export async function getCommentCheckerPath(): Promise<string | null> {
|
||||||
|
// 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 {
|
export interface HookInput {
|
||||||
session_id: string
|
session_id: string
|
||||||
@@ -114,17 +194,29 @@ export interface CheckResult {
|
|||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runCommentChecker(input: HookInput): Promise<CheckResult> {
|
/**
|
||||||
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<CheckResult> {
|
||||||
|
const binaryPath = cliPath ?? resolvedCliPath ?? COMMENT_CHECKER_CLI_PATH
|
||||||
|
|
||||||
|
if (!binaryPath) {
|
||||||
debugLog("comment-checker binary not found")
|
debugLog("comment-checker binary not found")
|
||||||
return { hasComments: false, message: "" }
|
return { hasComments: false, message: "" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!existsSync(binaryPath)) {
|
||||||
|
debugLog("comment-checker binary does not exist:", binaryPath)
|
||||||
|
return { hasComments: false, message: "" }
|
||||||
|
}
|
||||||
|
|
||||||
const jsonInput = JSON.stringify(input)
|
const jsonInput = JSON.stringify(input)
|
||||||
debugLog("running comment-checker with input:", jsonInput.substring(0, 200))
|
debugLog("running comment-checker with input:", jsonInput.substring(0, 200))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const proc = spawn([COMMENT_CHECKER_CLI_PATH], {
|
const proc = spawn([binaryPath], {
|
||||||
stdin: "pipe",
|
stdin: "pipe",
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
@@ -158,6 +250,18 @@ export async function runCommentChecker(input: HookInput): Promise<CheckResult>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if CLI is available (sync check, no download).
|
||||||
|
*/
|
||||||
export function isCliAvailable(): boolean {
|
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<boolean> {
|
||||||
|
const path = await getCommentCheckerPath()
|
||||||
|
return path !== null && existsSync(path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +1,3 @@
|
|||||||
import type { LanguageConfig } from "./types"
|
|
||||||
|
|
||||||
export const EXTENSION_TO_LANGUAGE: Record<string, string> = {
|
|
||||||
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<string, string> = {
|
|
||||||
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<string, string> = {
|
|
||||||
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([
|
export const BDD_KEYWORDS = new Set([
|
||||||
"given",
|
"given",
|
||||||
"when",
|
"when",
|
||||||
@@ -191,14 +77,3 @@ Review in the above priority order and take the corresponding action EVERY TIME
|
|||||||
|
|
||||||
Detected comments/docstrings:
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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<unknown>
|
|
||||||
isInitializing: boolean
|
|
||||||
lastUsedAt: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
let parserClass: any = null
|
|
||||||
let parserInitPromise: Promise<void> | null = null
|
|
||||||
const languageCache = new Map<string, ManagedLanguage>()
|
|
||||||
|
|
||||||
const LANGUAGE_NAME_MAP: Record<string, string> = {
|
|
||||||
golang: "go",
|
|
||||||
csharp: "c_sharp",
|
|
||||||
cpp: "cpp",
|
|
||||||
}
|
|
||||||
|
|
||||||
const COMMON_LANGUAGES = [
|
|
||||||
"python",
|
|
||||||
"typescript",
|
|
||||||
"javascript",
|
|
||||||
"tsx",
|
|
||||||
"go",
|
|
||||||
"rust",
|
|
||||||
"java",
|
|
||||||
]
|
|
||||||
|
|
||||||
async function initParserClass(): Promise<void> {
|
|
||||||
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<unknown | null> {
|
|
||||||
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<unknown | null> {
|
|
||||||
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("<!--") || stripped.startsWith("--")) {
|
|
||||||
return "block"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "line"
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function detectComments(
|
|
||||||
filePath: string,
|
|
||||||
content: string,
|
|
||||||
includeDocstrings = true
|
|
||||||
): Promise<CommentInfo[]> {
|
|
||||||
debugLog("detectComments called:", { filePath, contentLength: content.length })
|
|
||||||
|
|
||||||
const langName = getLanguageByExtension(filePath)
|
|
||||||
if (!langName) {
|
|
||||||
debugLog("unsupported language for:", filePath)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryPattern = QUERY_TEMPLATES[langName]
|
|
||||||
if (!queryPattern) {
|
|
||||||
debugLog("no query pattern for:", langName)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parser = await getParser()
|
|
||||||
const language = await getLanguage(langName)
|
|
||||||
|
|
||||||
if (!language) {
|
|
||||||
debugLog("language not available:", langName)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
parser.setLanguage(language)
|
|
||||||
const tree = parser.parse(content)
|
|
||||||
const comments: CommentInfo[] = []
|
|
||||||
|
|
||||||
const query = (language as { query: (pattern: string) => { matches: (node: unknown) => Array<{ captures: Array<{ node: { text: string; type: string; startPosition: { row: number } } }> }> } }).query(queryPattern)
|
|
||||||
const matches = query.matches(tree.rootNode)
|
|
||||||
|
|
||||||
for (const match of matches) {
|
|
||||||
for (const capture of match.captures) {
|
|
||||||
const node = capture.node
|
|
||||||
const text = node.text
|
|
||||||
const lineNumber = node.startPosition.row + 1
|
|
||||||
|
|
||||||
const commentType = determineCommentType(text, node.type)
|
|
||||||
const isDocstring = commentType === "docstring"
|
|
||||||
|
|
||||||
if (isDocstring && !includeDocstrings) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
comments.push({
|
|
||||||
text,
|
|
||||||
lineNumber,
|
|
||||||
filePath,
|
|
||||||
commentType,
|
|
||||||
isDocstring,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeDocstrings) {
|
|
||||||
const docQuery = DOCSTRING_QUERIES[langName]
|
|
||||||
if (docQuery) {
|
|
||||||
try {
|
|
||||||
const docQueryObj = (language as { query: (pattern: string) => { matches: (node: unknown) => Array<{ captures: Array<{ node: { text: string; startPosition: { row: number } } }> }> } }).query(docQuery)
|
|
||||||
const docMatches = docQueryObj.matches(tree.rootNode)
|
|
||||||
|
|
||||||
for (const match of docMatches) {
|
|
||||||
for (const capture of match.captures) {
|
|
||||||
const node = capture.node
|
|
||||||
const text = node.text
|
|
||||||
const lineNumber = node.startPosition.row + 1
|
|
||||||
|
|
||||||
const alreadyAdded = comments.some(
|
|
||||||
(c) => c.lineNumber === lineNumber && c.text === text
|
|
||||||
)
|
|
||||||
if (!alreadyAdded) {
|
|
||||||
comments.push({
|
|
||||||
text,
|
|
||||||
lineNumber,
|
|
||||||
filePath,
|
|
||||||
commentType: "docstring",
|
|
||||||
isDocstring: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
comments.sort((a, b) => a.lineNumber - b.lineNumber)
|
|
||||||
|
|
||||||
debugLog("detected comments:", comments.length)
|
|
||||||
return comments
|
|
||||||
} catch (err) {
|
|
||||||
debugLog("detectComments failed:", err)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
210
src/hooks/comment-checker/downloader.ts
Normal file
210
src/hooks/comment-checker/downloader.ts
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import { spawn } from "bun"
|
||||||
|
import { existsSync, mkdirSync, chmodSync, unlinkSync, appendFileSync } from "fs"
|
||||||
|
import { join } from "path"
|
||||||
|
import { homedir } from "os"
|
||||||
|
import { createRequire } from "module"
|
||||||
|
|
||||||
|
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:downloader] ${args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' ')}\n`
|
||||||
|
appendFileSync(DEBUG_FILE, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const REPO = "code-yeongyu/go-claude-code-comment-checker"
|
||||||
|
|
||||||
|
interface PlatformInfo {
|
||||||
|
os: string
|
||||||
|
arch: string
|
||||||
|
ext: "tar.gz" | "zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
const PLATFORM_MAP: Record<string, PlatformInfo> = {
|
||||||
|
"darwin-arm64": { os: "darwin", arch: "arm64", ext: "tar.gz" },
|
||||||
|
"darwin-x64": { os: "darwin", arch: "amd64", ext: "tar.gz" },
|
||||||
|
"linux-arm64": { os: "linux", arch: "arm64", ext: "tar.gz" },
|
||||||
|
"linux-x64": { os: "linux", arch: "amd64", ext: "tar.gz" },
|
||||||
|
"win32-x64": { os: "windows", arch: "amd64", ext: "zip" },
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cache directory for oh-my-opencode binaries.
|
||||||
|
* Follows XDG Base Directory Specification.
|
||||||
|
*/
|
||||||
|
export function getCacheDir(): string {
|
||||||
|
const xdgCache = process.env.XDG_CACHE_HOME
|
||||||
|
const base = xdgCache || join(homedir(), ".cache")
|
||||||
|
return join(base, "oh-my-opencode", "bin")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the binary name based on platform.
|
||||||
|
*/
|
||||||
|
export function getBinaryName(): string {
|
||||||
|
return process.platform === "win32" ? "comment-checker.exe" : "comment-checker"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the cached binary path if it exists.
|
||||||
|
*/
|
||||||
|
export function getCachedBinaryPath(): string | null {
|
||||||
|
const binaryPath = join(getCacheDir(), getBinaryName())
|
||||||
|
return existsSync(binaryPath) ? binaryPath : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the version from the installed @code-yeongyu/comment-checker package.
|
||||||
|
*/
|
||||||
|
function getPackageVersion(): string {
|
||||||
|
try {
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
const pkg = require("@code-yeongyu/comment-checker/package.json")
|
||||||
|
return pkg.version
|
||||||
|
} catch {
|
||||||
|
// Fallback to hardcoded version if package not found
|
||||||
|
return "0.4.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract tar.gz archive using system tar command.
|
||||||
|
*/
|
||||||
|
async function extractTarGz(archivePath: string, destDir: string): Promise<void> {
|
||||||
|
debugLog("Extracting tar.gz:", archivePath, "to", destDir)
|
||||||
|
|
||||||
|
const proc = spawn(["tar", "-xzf", archivePath, "-C", destDir], {
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
})
|
||||||
|
|
||||||
|
const exitCode = await proc.exited
|
||||||
|
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
const stderr = await new Response(proc.stderr).text()
|
||||||
|
throw new Error(`tar extraction failed (exit ${exitCode}): ${stderr}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract zip archive using system commands.
|
||||||
|
*/
|
||||||
|
async function extractZip(archivePath: string, destDir: string): Promise<void> {
|
||||||
|
debugLog("Extracting zip:", archivePath, "to", destDir)
|
||||||
|
|
||||||
|
const proc = process.platform === "win32"
|
||||||
|
? spawn(["powershell", "-command", `Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force`], {
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
})
|
||||||
|
: spawn(["unzip", "-o", archivePath, "-d", destDir], {
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
})
|
||||||
|
|
||||||
|
const exitCode = await proc.exited
|
||||||
|
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
const stderr = await new Response(proc.stderr).text()
|
||||||
|
throw new Error(`zip extraction failed (exit ${exitCode}): ${stderr}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the comment-checker binary from GitHub Releases.
|
||||||
|
* Returns the path to the downloaded binary, or null on failure.
|
||||||
|
*/
|
||||||
|
export async function downloadCommentChecker(): Promise<string | null> {
|
||||||
|
const platformKey = `${process.platform}-${process.arch}`
|
||||||
|
const platformInfo = PLATFORM_MAP[platformKey]
|
||||||
|
|
||||||
|
if (!platformInfo) {
|
||||||
|
debugLog(`Unsupported platform: ${platformKey}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheDir = getCacheDir()
|
||||||
|
const binaryName = getBinaryName()
|
||||||
|
const binaryPath = join(cacheDir, binaryName)
|
||||||
|
|
||||||
|
// Already exists in cache
|
||||||
|
if (existsSync(binaryPath)) {
|
||||||
|
debugLog("Binary already cached at:", binaryPath)
|
||||||
|
return binaryPath
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = getPackageVersion()
|
||||||
|
const { os, arch, ext } = platformInfo
|
||||||
|
const assetName = `comment-checker_v${version}_${os}_${arch}.${ext}`
|
||||||
|
const downloadUrl = `https://github.com/${REPO}/releases/download/v${version}/${assetName}`
|
||||||
|
|
||||||
|
debugLog(`Downloading from: ${downloadUrl}`)
|
||||||
|
console.log(`[oh-my-opencode] Downloading comment-checker binary...`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ensure cache directory exists
|
||||||
|
if (!existsSync(cacheDir)) {
|
||||||
|
mkdirSync(cacheDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download with fetch() - Bun handles redirects automatically
|
||||||
|
const response = await fetch(downloadUrl, { redirect: "follow" })
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const archivePath = join(cacheDir, assetName)
|
||||||
|
const arrayBuffer = await response.arrayBuffer()
|
||||||
|
await Bun.write(archivePath, arrayBuffer)
|
||||||
|
|
||||||
|
debugLog(`Downloaded archive to: ${archivePath}`)
|
||||||
|
|
||||||
|
// Extract based on file type
|
||||||
|
if (ext === "tar.gz") {
|
||||||
|
await extractTarGz(archivePath, cacheDir)
|
||||||
|
} else {
|
||||||
|
await extractZip(archivePath, cacheDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up archive
|
||||||
|
if (existsSync(archivePath)) {
|
||||||
|
unlinkSync(archivePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set execute permission on Unix
|
||||||
|
if (process.platform !== "win32" && existsSync(binaryPath)) {
|
||||||
|
chmodSync(binaryPath, 0o755)
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog(`Successfully downloaded binary to: ${binaryPath}`)
|
||||||
|
console.log(`[oh-my-opencode] comment-checker binary ready.`)
|
||||||
|
|
||||||
|
return binaryPath
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
debugLog(`Failed to download: ${err}`)
|
||||||
|
console.error(`[oh-my-opencode] Failed to download comment-checker: ${err instanceof Error ? err.message : err}`)
|
||||||
|
console.error(`[oh-my-opencode] Comment checking disabled.`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the comment-checker binary is available.
|
||||||
|
* First checks cache, then downloads if needed.
|
||||||
|
* Returns the binary path or null if unavailable.
|
||||||
|
*/
|
||||||
|
export async function ensureCommentCheckerBinary(): Promise<string | null> {
|
||||||
|
// Check cache first
|
||||||
|
const cachedPath = getCachedBinaryPath()
|
||||||
|
if (cachedPath) {
|
||||||
|
debugLog("Using cached binary:", cachedPath)
|
||||||
|
return cachedPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download if not cached
|
||||||
|
return downloadCommentChecker()
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import type { PendingCall, FileComments } from "./types"
|
import type { PendingCall } from "./types"
|
||||||
import { runCommentChecker, isCliAvailable, type HookInput } from "./cli"
|
import { runCommentChecker, getCommentCheckerPath, startBackgroundInit, type HookInput } from "./cli"
|
||||||
import { detectComments, isSupportedFile, warmupCommonLanguages } from "./detector"
|
|
||||||
import { applyFilters } from "./filters"
|
|
||||||
import { formatHookMessage } from "./output"
|
|
||||||
|
|
||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
|
import { existsSync } from "fs"
|
||||||
|
|
||||||
const DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1"
|
const DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1"
|
||||||
const DEBUG_FILE = "/tmp/comment-checker-debug.log"
|
const DEBUG_FILE = "/tmp/comment-checker-debug.log"
|
||||||
@@ -19,9 +17,7 @@ function debugLog(...args: unknown[]) {
|
|||||||
const pendingCalls = new Map<string, PendingCall>()
|
const pendingCalls = new Map<string, PendingCall>()
|
||||||
const PENDING_CALL_TTL = 60_000
|
const PENDING_CALL_TTL = 60_000
|
||||||
|
|
||||||
// Check if native CLI is available at startup
|
let cliPathPromise: Promise<string | null> | null = null
|
||||||
const USE_CLI = isCliAvailable()
|
|
||||||
debugLog("comment-checker mode:", USE_CLI ? "CLI (native)" : "WASM (fallback)")
|
|
||||||
|
|
||||||
function cleanupOldPendingCalls(): void {
|
function cleanupOldPendingCalls(): void {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
@@ -37,10 +33,14 @@ setInterval(cleanupOldPendingCalls, 10_000)
|
|||||||
export function createCommentCheckerHooks() {
|
export function createCommentCheckerHooks() {
|
||||||
debugLog("createCommentCheckerHooks called")
|
debugLog("createCommentCheckerHooks called")
|
||||||
|
|
||||||
// Background warmup for WASM fallback - LSP style (non-blocking)
|
// Start background CLI initialization (may trigger lazy download)
|
||||||
if (!USE_CLI) {
|
startBackgroundInit()
|
||||||
warmupCommonLanguages()
|
cliPathPromise = getCommentCheckerPath()
|
||||||
}
|
cliPathPromise.then(path => {
|
||||||
|
debugLog("CLI path resolved:", path || "disabled (no binary)")
|
||||||
|
}).catch(err => {
|
||||||
|
debugLog("CLI path resolution error:", err)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"tool.execute.before": async (
|
"tool.execute.before": async (
|
||||||
@@ -68,11 +68,6 @@ export function createCommentCheckerHooks() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!USE_CLI && !isSupportedFile(filePath)) {
|
|
||||||
debugLog("unsupported file:", filePath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog("registering pendingCall:", { callID: input.callID, filePath, tool: toolLower })
|
debugLog("registering pendingCall:", { callID: input.callID, filePath, tool: toolLower })
|
||||||
pendingCalls.set(input.callID, {
|
pendingCalls.set(input.callID, {
|
||||||
filePath,
|
filePath,
|
||||||
@@ -115,13 +110,18 @@ export function createCommentCheckerHooks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (USE_CLI) {
|
// Wait for CLI path resolution
|
||||||
// Native CLI mode - much faster
|
const cliPath = await cliPathPromise
|
||||||
await processWithCli(input, pendingCall, output)
|
|
||||||
} else {
|
if (!cliPath || !existsSync(cliPath)) {
|
||||||
// WASM fallback mode
|
// CLI not available - silently skip comment checking
|
||||||
await processWithWasm(pendingCall, output)
|
debugLog("CLI not available, skipping comment check")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CLI mode only
|
||||||
|
debugLog("using CLI:", cliPath)
|
||||||
|
await processWithCli(input, pendingCall, output, cliPath)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debugLog("tool.execute.after failed:", err)
|
debugLog("tool.execute.after failed:", err)
|
||||||
}
|
}
|
||||||
@@ -132,13 +132,14 @@ export function createCommentCheckerHooks() {
|
|||||||
async function processWithCli(
|
async function processWithCli(
|
||||||
input: { tool: string; sessionID: string; callID: string },
|
input: { tool: string; sessionID: string; callID: string },
|
||||||
pendingCall: PendingCall,
|
pendingCall: PendingCall,
|
||||||
output: { output: string }
|
output: { output: string },
|
||||||
|
cliPath: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
debugLog("using CLI mode")
|
debugLog("using CLI mode with path:", cliPath)
|
||||||
|
|
||||||
const hookInput: HookInput = {
|
const hookInput: HookInput = {
|
||||||
session_id: pendingCall.sessionID,
|
session_id: pendingCall.sessionID,
|
||||||
tool_name: pendingCall.tool.charAt(0).toUpperCase() + pendingCall.tool.slice(1), // "write" -> "Write"
|
tool_name: pendingCall.tool.charAt(0).toUpperCase() + pendingCall.tool.slice(1),
|
||||||
transcript_path: "",
|
transcript_path: "",
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
hook_event_name: "PostToolUse",
|
hook_event_name: "PostToolUse",
|
||||||
@@ -151,7 +152,7 @@ async function processWithCli(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await runCommentChecker(hookInput)
|
const result = await runCommentChecker(hookInput, cliPath)
|
||||||
|
|
||||||
if (result.hasComments && result.message) {
|
if (result.hasComments && result.message) {
|
||||||
debugLog("CLI detected comments, appending message")
|
debugLog("CLI detected comments, appending message")
|
||||||
@@ -160,47 +161,3 @@ async function processWithCli(
|
|||||||
debugLog("CLI: no comments detected")
|
debugLog("CLI: no comments detected")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processWithWasm(
|
|
||||||
pendingCall: PendingCall,
|
|
||||||
output: { output: string }
|
|
||||||
): Promise<void> {
|
|
||||||
debugLog("using WASM fallback mode")
|
|
||||||
|
|
||||||
let content: string
|
|
||||||
|
|
||||||
if (pendingCall.content) {
|
|
||||||
content = pendingCall.content
|
|
||||||
debugLog("using content from args")
|
|
||||||
} else {
|
|
||||||
debugLog("reading file:", pendingCall.filePath)
|
|
||||||
const file = Bun.file(pendingCall.filePath)
|
|
||||||
content = await file.text()
|
|
||||||
debugLog("file content length:", content.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
debugLog("calling detectComments...")
|
|
||||||
const rawComments = await detectComments(pendingCall.filePath, content)
|
|
||||||
debugLog("raw comments:", rawComments.length)
|
|
||||||
|
|
||||||
const filteredComments = applyFilters(rawComments)
|
|
||||||
debugLog("filtered comments:", filteredComments.length)
|
|
||||||
|
|
||||||
if (filteredComments.length === 0) {
|
|
||||||
debugLog("no comments after filtering")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileComments: FileComments[] = [
|
|
||||||
{
|
|
||||||
filePath: pendingCall.filePath,
|
|
||||||
comments: filteredComments,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const message = formatHookMessage(fileComments)
|
|
||||||
debugLog("appending message to output")
|
|
||||||
output.output += `\n\n${message}`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,6 @@ export interface CommentInfo {
|
|||||||
metadata?: Record<string, string>
|
metadata?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LanguageConfig {
|
|
||||||
extensions: string[]
|
|
||||||
commentQuery: string
|
|
||||||
docstringQuery?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PendingCall {
|
export interface PendingCall {
|
||||||
filePath: string
|
filePath: string
|
||||||
content?: string
|
content?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user