mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-02-07 23:23:08 +08:00
Major OpenCode integration overhaul: - llms.txt: Comprehensive OpenCode documentation for LLMs (642 lines) - .opencode/plugins/ecc-hooks.ts: All Claude Code hooks translated to OpenCode's plugin system - .opencode/tools/*.ts: 3 custom tools (run-tests, check-coverage, security-audit) - .opencode/commands/*.md: All 24 commands in OpenCode format - .opencode/package.json: npm package structure for opencode-ecc - .opencode/index.ts: Main plugin entry point - Delete incorrect LIMITATIONS.md (hooks ARE supported via plugins) - Rewrite MIGRATION.md with correct hook event mapping - Update README.md OpenCode section to show full feature parity OpenCode has 20+ events vs Claude Code's 3 phases: - PreToolUse → tool.execute.before - PostToolUse → tool.execute.after - Stop → session.idle - SessionStart → session.created - SessionEnd → session.deleted - Plus: file.edited, file.watcher.updated, permission.asked, todo.updated - 12 agents: Full parity - 24 commands: Full parity (+1 from original 23) - 16 skills: Full parity - Hooks: OpenCode has MORE (20+ events vs 3 phases) - Custom Tools: 3 native OpenCode tools The OpenCode configuration can now be: 1. Used directly: cd everything-claude-code && opencode 2. Installed via npm: npm install opencode-ecc
171 lines
4.7 KiB
TypeScript
171 lines
4.7 KiB
TypeScript
/**
|
|
* Check Coverage Tool
|
|
*
|
|
* Custom OpenCode tool to analyze test coverage and report on gaps.
|
|
* Supports common coverage report formats.
|
|
*/
|
|
|
|
import { tool } from "@opencode-ai/plugin"
|
|
import * as path from "path"
|
|
import * as fs from "fs"
|
|
|
|
export default tool({
|
|
description:
|
|
"Check test coverage against a threshold and identify files with low coverage. Reads coverage reports from common locations.",
|
|
args: {
|
|
threshold: tool.schema
|
|
.number()
|
|
.optional()
|
|
.describe("Minimum coverage percentage required (default: 80)"),
|
|
showUncovered: tool.schema
|
|
.boolean()
|
|
.optional()
|
|
.describe("Show list of uncovered files (default: true)"),
|
|
format: tool.schema
|
|
.enum(["summary", "detailed", "json"])
|
|
.optional()
|
|
.describe("Output format (default: summary)"),
|
|
},
|
|
async execute(args, context) {
|
|
const threshold = args.threshold ?? 80
|
|
const showUncovered = args.showUncovered ?? true
|
|
const format = args.format ?? "summary"
|
|
const cwd = context.worktree || context.directory
|
|
|
|
// Look for coverage reports
|
|
const coveragePaths = [
|
|
"coverage/coverage-summary.json",
|
|
"coverage/lcov-report/index.html",
|
|
"coverage/coverage-final.json",
|
|
".nyc_output/coverage.json",
|
|
]
|
|
|
|
let coverageData: CoverageSummary | null = null
|
|
let coverageFile: string | null = null
|
|
|
|
for (const coveragePath of coveragePaths) {
|
|
const fullPath = path.join(cwd, coveragePath)
|
|
if (fs.existsSync(fullPath) && coveragePath.endsWith(".json")) {
|
|
try {
|
|
const content = JSON.parse(fs.readFileSync(fullPath, "utf-8"))
|
|
coverageData = parseCoverageData(content)
|
|
coverageFile = coveragePath
|
|
break
|
|
} catch {
|
|
// Continue to next file
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!coverageData) {
|
|
return {
|
|
success: false,
|
|
error: "No coverage report found",
|
|
suggestion:
|
|
"Run tests with coverage first: npm test -- --coverage",
|
|
searchedPaths: coveragePaths,
|
|
}
|
|
}
|
|
|
|
const passed = coverageData.total.percentage >= threshold
|
|
const uncoveredFiles = coverageData.files.filter(
|
|
(f) => f.percentage < threshold
|
|
)
|
|
|
|
const result: CoverageResult = {
|
|
success: passed,
|
|
threshold,
|
|
coverageFile,
|
|
total: coverageData.total,
|
|
passed,
|
|
}
|
|
|
|
if (format === "detailed" || (showUncovered && uncoveredFiles.length > 0)) {
|
|
result.uncoveredFiles = uncoveredFiles.slice(0, 20) // Limit to 20 files
|
|
result.uncoveredCount = uncoveredFiles.length
|
|
}
|
|
|
|
if (format === "json") {
|
|
result.rawData = coverageData
|
|
}
|
|
|
|
if (!passed) {
|
|
result.suggestion = `Coverage is ${coverageData.total.percentage.toFixed(1)}% which is below the ${threshold}% threshold. Focus on these files:\n${uncoveredFiles
|
|
.slice(0, 5)
|
|
.map((f) => `- ${f.file}: ${f.percentage.toFixed(1)}%`)
|
|
.join("\n")}`
|
|
}
|
|
|
|
return result
|
|
},
|
|
})
|
|
|
|
interface CoverageSummary {
|
|
total: {
|
|
lines: number
|
|
covered: number
|
|
percentage: number
|
|
}
|
|
files: Array<{
|
|
file: string
|
|
lines: number
|
|
covered: number
|
|
percentage: number
|
|
}>
|
|
}
|
|
|
|
interface CoverageResult {
|
|
success: boolean
|
|
threshold: number
|
|
coverageFile: string | null
|
|
total: CoverageSummary["total"]
|
|
passed: boolean
|
|
uncoveredFiles?: CoverageSummary["files"]
|
|
uncoveredCount?: number
|
|
rawData?: CoverageSummary
|
|
suggestion?: string
|
|
}
|
|
|
|
function parseCoverageData(data: unknown): CoverageSummary {
|
|
// Handle istanbul/nyc format
|
|
if (typeof data === "object" && data !== null && "total" in data) {
|
|
const istanbulData = data as Record<string, unknown>
|
|
const total = istanbulData.total as Record<string, { total: number; covered: number }>
|
|
|
|
const files: CoverageSummary["files"] = []
|
|
|
|
for (const [key, value] of Object.entries(istanbulData)) {
|
|
if (key !== "total" && typeof value === "object" && value !== null) {
|
|
const fileData = value as Record<string, { total: number; covered: number }>
|
|
if (fileData.lines) {
|
|
files.push({
|
|
file: key,
|
|
lines: fileData.lines.total,
|
|
covered: fileData.lines.covered,
|
|
percentage: fileData.lines.total > 0
|
|
? (fileData.lines.covered / fileData.lines.total) * 100
|
|
: 100,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
total: {
|
|
lines: total.lines?.total || 0,
|
|
covered: total.lines?.covered || 0,
|
|
percentage: total.lines?.total
|
|
? (total.lines.covered / total.lines.total) * 100
|
|
: 0,
|
|
},
|
|
files,
|
|
}
|
|
}
|
|
|
|
// Default empty result
|
|
return {
|
|
total: { lines: 0, covered: 0, percentage: 0 },
|
|
files: [],
|
|
}
|
|
}
|