refactor(packages): extract comment-checker-core package
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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: "" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user