refactor(packages): extract comment-checker-core package

This commit is contained in:
YeonGyu-Kim
2026-05-21 01:17:04 +09:00
parent 3b303e79f4
commit 7028c1f40a
14 changed files with 562 additions and 355 deletions
+19 -178
View File
@@ -1,180 +1,21 @@
import type { ApplyPatchEdit } from "./cli-runner"
import {
extractApplyPatchEdits,
getApplyPatchMetadataFiles,
getString,
isRecord,
joinPatchLines,
makeAccumulator,
parseApplyPatchRequests,
readApplyPatchMetadataFiles,
} from "@oh-my-opencode/comment-checker-core"
type ApplyPatchFileMetadata = {
readonly filePath: string
readonly movePath?: string
readonly before: string
readonly after: string
readonly type?: string
}
type ApplyPatchAccumulator = {
operation: "add" | "update" | "delete"
filePath: string
movePath?: string
oldLines: string[]
newLines: string[]
}
export function extractApplyPatchEdits(
details: unknown,
args?: Record<string, unknown>,
): ApplyPatchEdit[] {
const metadataEdits = getApplyPatchMetadataFiles(details)
.filter((file) => file.type?.toLowerCase() !== "delete")
.map((file) => ({
filePath: file.movePath ?? file.filePath,
before: file.before,
after: file.after,
}))
if (metadataEdits.length > 0) return metadataEdits
const patch = args === undefined ? undefined : getString(args, ["patchText", "input", "patch", "command"])
if (patch === undefined) return []
return parseApplyPatchEdits(patch)
}
function getApplyPatchMetadataFiles(details: unknown): ApplyPatchFileMetadata[] {
if (!isRecord(details)) return []
const direct = readApplyPatchMetadataFiles(details["files"])
if (direct.length > 0) return direct
const resultDetails = details["result"]
const result = isRecord(resultDetails) ? readApplyPatchMetadataFiles(resultDetails["files"]) : []
if (result.length > 0) return result
const metadataDetails = details["metadata"]
return isRecord(metadataDetails) ? readApplyPatchMetadataFiles(metadataDetails["files"]) : []
}
function readApplyPatchMetadataFiles(value: unknown): ApplyPatchFileMetadata[] {
if (!Array.isArray(value)) return []
const files: ApplyPatchFileMetadata[] = []
for (const item of value) {
if (!isRecord(item)) continue
const filePath = getString(item, ["filePath", "file_path", "path"])
const movePath = getString(item, ["movePath", "move_path"])
const before = getString(item, ["before", "old", "oldString", "old_string"])
const after = getString(item, ["after", "new", "newString", "new_string"])
const type = getString(item, ["type", "operation"])
if (filePath === undefined || before === undefined || after === undefined) continue
files.push({
filePath,
before,
after,
...(movePath === undefined ? {} : { movePath }),
...(type === undefined ? {} : { type }),
})
}
return files
}
function parseApplyPatchEdits(patch: string): ApplyPatchEdit[] {
const edits: ApplyPatchEdit[] = []
let current: ApplyPatchAccumulator | undefined
const flush = (): void => {
if (current === undefined) return
if (current.operation === "add") {
const after = joinPatchLines(current.newLines)
if (after.length > 0) {
edits.push({ filePath: current.filePath, before: "", after })
}
}
if (current.operation === "update") {
const after = joinPatchLines(current.newLines)
if (after.length > 0) {
edits.push({
filePath: current.movePath ?? current.filePath,
before: joinPatchLines(current.oldLines),
after,
})
}
}
current = undefined
}
for (const line of patch.split(/\r?\n/)) {
if (line === "*** Begin Patch" || line === "*** End Patch") continue
if (line.startsWith("*** Add File: ")) {
flush()
current = makeAccumulator("add", line.slice("*** Add File: ".length).trim())
continue
}
if (line.startsWith("*** Update File: ")) {
flush()
current = makeAccumulator("update", line.slice("*** Update File: ".length).trim())
continue
}
if (line.startsWith("*** Delete File: ")) {
flush()
current = makeAccumulator("delete", line.slice("*** Delete File: ".length).trim())
continue
}
if (line.startsWith("*** Move to: ")) {
if (current?.operation === "update") {
current.movePath = line.slice("*** Move to: ".length).trim()
}
continue
}
if (current === undefined || line.startsWith("@@")) continue
if (current.operation === "add") {
if (line.startsWith("+")) current.newLines.push(line.slice(1))
continue
}
if (current.operation === "update") {
if (line.startsWith("-")) current.oldLines.push(line.slice(1))
if (line.startsWith("+")) current.newLines.push(line.slice(1))
}
}
flush()
return edits
}
function makeAccumulator(
operation: ApplyPatchAccumulator["operation"],
filePath: string,
): ApplyPatchAccumulator {
return {
operation,
filePath,
oldLines: [],
newLines: [],
}
}
function getString(input: Record<string, unknown>, keys: readonly string[]): string | undefined {
for (const key of keys) {
const value = input[key]
if (typeof value === "string") return value
}
return undefined
}
function joinPatchLines(lines: readonly string[]): string {
return lines.length === 0 ? "" : `${lines.join("\n")}\n`
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
export {
extractApplyPatchEdits,
getApplyPatchMetadataFiles,
getString,
isRecord,
joinPatchLines,
makeAccumulator,
parseApplyPatchRequests,
readApplyPatchMetadataFiles,
}
+33 -141
View File
@@ -1,9 +1,14 @@
import { spawn } from "../../shared/bun-spawn-shim"
import { createRequire } from "module"
import { dirname, join } from "path"
import { join } from "path"
import { existsSync } from "fs"
import * as fs from "fs"
import { tmpdir } from "os"
import {
resolveCommentCheckerBinary,
runCommentChecker as runCommentCheckerCore,
type CheckResult,
type HookInput,
} from "@oh-my-opencode/comment-checker-core"
import { getCachedBinaryPath, ensureCommentCheckerBinary } from "./downloader"
const DEBUG = process.env.COMMENT_CHECKER_DEBUG === "1"
@@ -21,33 +26,15 @@ function getBinaryName(): string {
}
function findCommentCheckerPathSync(): string | null {
const binaryName = getBinaryName()
// Check cached binary first (safest path - no module resolution needed)
const cachedPath = getCachedBinaryPath()
if (cachedPath) {
debugLog("found binary in cache:", cachedPath)
return cachedPath
}
// Guard against undefined import.meta.url (can happen on Windows during plugin loading)
if (!import.meta.url) {
debugLog("import.meta.url is undefined, skipping package resolution")
return null
}
try {
const require = createRequire(import.meta.url)
const cliPkgPath = require.resolve("@code-yeongyu/comment-checker/package.json")
const cliDir = dirname(cliPkgPath)
const binaryPath = join(cliDir, "bin", binaryName)
if (existsSync(binaryPath)) {
debugLog("found binary in main package:", binaryPath)
return binaryPath
}
} catch (err) {
debugLog("main package not installed or resolution failed:", err)
const resolvedPath = resolveCommentCheckerBinary({
binaryName: getBinaryName(),
cachedBinaryPath: getCachedBinaryPath(),
existsSync,
importMetaUrl: import.meta.url,
})
if (resolvedPath !== null) {
debugLog("resolved binary path:", resolvedPath)
return resolvedPath
}
debugLog("no binary found in known locations")
@@ -121,26 +108,7 @@ export function startBackgroundInit(): void {
}
}
export interface HookInput {
session_id: string
tool_name: string
transcript_path: string
cwd: string
hook_event_name: string
tool_input: {
file_path?: string
content?: string
old_string?: string
new_string?: string
edits?: Array<{ old_string: string; new_string: string }>
}
tool_response?: unknown
}
export interface CheckResult {
hasComments: boolean
message: string
}
export type { HookInput, CheckResult }
/**
* Run comment-checker CLI with given input.
@@ -150,104 +118,28 @@ export interface CheckResult {
*/
export async function runCommentChecker(input: HookInput, cliPath?: string, customPrompt?: string): Promise<CheckResult> {
const binaryPath = cliPath ?? resolvedCliPath ?? getCommentCheckerPathSync()
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))
let didTimeout = false
try {
const args = [binaryPath, "check"]
if (customPrompt) {
args.push("--prompt", customPrompt)
}
const proc = spawn(args, {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
})
let timeoutId: ReturnType<typeof setTimeout> | null = null
const timeoutPromise = new Promise<"timeout">(resolve => {
timeoutId = setTimeout(async () => {
didTimeout = true
debugLog("comment-checker timed out after 30s; sending SIGTERM")
try {
proc.kill("SIGTERM")
} catch (err) {
debugLog("failed to SIGTERM:", err)
}
const graceTimer = setTimeout(() => {
try {
proc.kill("SIGKILL")
debugLog("sent SIGKILL after grace period")
} catch {
}
}, 1000)
try {
await proc.exited
} catch {
}
clearTimeout(graceTimer)
resolve("timeout")
}, 30_000)
})
try {
// Write JSON to stdin
proc.stdin.write(jsonInput)
proc.stdin.end()
const stdoutPromise = new Response(proc.stdout).text()
const stderrPromise = new Response(proc.stderr).text()
const exitCodePromise = proc.exited
const raceResult = await Promise.race([
Promise.all([stdoutPromise, stderrPromise, exitCodePromise] as const),
timeoutPromise,
])
if (raceResult === "timeout") {
return { hasComments: false, message: "" }
}
const [stdout, stderr, exitCode] = raceResult
debugLog("exit code:", exitCode, "stdout length:", stdout.length, "stderr length:", stderr.length)
if (exitCode === 0) {
return { hasComments: false, message: "" }
}
if (exitCode === 2) {
// Comments detected - message is in stderr
return { hasComments: true, message: stderr }
}
// Error case
debugLog("unexpected exit code:", exitCode, "stderr:", stderr)
return { hasComments: false, message: "" }
} finally {
if (timeoutId !== null) {
clearTimeout(timeoutId)
}
}
} catch (err) {
if (didTimeout) {
return { hasComments: false, message: "" }
}
debugLog("failed to run comment-checker:", err)
const result = await runCommentCheckerCore(
{ hookInput: input, binaryPath, customPrompt },
{
existsSync,
spawn: (args: readonly string[]) =>
spawn([...args], {
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
}),
},
)
return result
} catch (error) {
debugLog("failed to run comment-checker:", error)
return { hasComments: false, message: "" }
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ import {
processWithCli,
processApplyPatchEditsWithCli,
} from "./cli-runner"
import { extractApplyPatchEdits } from "./apply-patch-edits"
import { extractApplyPatchEdits } from "@oh-my-opencode/comment-checker-core"
import {
registerPendingCall,
startPendingCallCleanup,
+19 -33
View File
@@ -1,33 +1,19 @@
export type CommentType = "line" | "block" | "docstring"
export interface CommentInfo {
text: string
lineNumber: number
filePath: string
commentType: CommentType
isDocstring: boolean
metadata?: Record<string, string>
}
export interface PendingCall {
filePath: string
content?: string
oldString?: string
newString?: string
edits?: Array<{ old_string: string; new_string: string }>
tool: "write" | "edit" | "multiedit"
sessionID: string
timestamp: number
}
export interface FileComments {
filePath: string
comments: CommentInfo[]
}
export interface FilterResult {
shouldSkip: boolean
reason?: string
}
export type CommentFilter = (comment: CommentInfo) => FilterResult
export type {
ApplyPatchAccumulator,
ApplyPatchFileMetadata,
CheckResult,
CheckerEdit,
CommentFilter,
CommentInfo,
CommentType,
FileComments,
FilterResult,
HookInput,
PendingCall,
ResolveCommentCheckerBinaryInput,
RunCommentCheckerInput,
RunCommentCheckerOptions,
SpawnFn,
SpawnProcess,
SpawnSignal,
} from "@oh-my-opencode/comment-checker-core"