chore: import upstream snapshot with attribution
CI / Test (ubuntu-latest, Node 18.x, bun) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, npm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, pnpm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, yarn) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 20.x, bun) (push) Failing after 17m13s
CI / Test (ubuntu-latest, Node 20.x, npm) (push) Failing after 18m42s
CI / Test (ubuntu-latest, Node 20.x, pnpm) (push) Failing after 15m0s
CI / Test (ubuntu-latest, Node 20.x, yarn) (push) Failing after 49m44s
CI / Test (ubuntu-latest, Node 22.x, bun) (push) Failing after 51m55s
CI / Test (ubuntu-latest, Node 22.x, pnpm) (push) Failing after 21m57s
CI / Test (ubuntu-latest, Node 22.x, npm) (push) Failing after 37m39s
CI / Test (ubuntu-latest, Node 22.x, yarn) (push) Failing after 34m7s
CI / Validate Components (push) Failing after 37m15s
CI / Python Tests (push) Failing after 10m1s
CI / Security Scan (push) Failing after 10m1s
CI / Lint (push) Failing after 17m12s
CI / Coverage (push) Failing after 20m19s
CI / Test (macos-latest, Node 18.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (ubuntu-latest, Node 18.x, bun) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, npm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, pnpm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, yarn) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 20.x, bun) (push) Failing after 17m13s
CI / Test (ubuntu-latest, Node 20.x, npm) (push) Failing after 18m42s
CI / Test (ubuntu-latest, Node 20.x, pnpm) (push) Failing after 15m0s
CI / Test (ubuntu-latest, Node 20.x, yarn) (push) Failing after 49m44s
CI / Test (ubuntu-latest, Node 22.x, bun) (push) Failing after 51m55s
CI / Test (ubuntu-latest, Node 22.x, pnpm) (push) Failing after 21m57s
CI / Test (ubuntu-latest, Node 22.x, npm) (push) Failing after 37m39s
CI / Test (ubuntu-latest, Node 22.x, yarn) (push) Failing after 34m7s
CI / Validate Components (push) Failing after 37m15s
CI / Python Tests (push) Failing after 10m1s
CI / Security Scan (push) Failing after 10m1s
CI / Lint (push) Failing after 17m12s
CI / Coverage (push) Failing after 20m19s
CI / Test (macos-latest, Node 18.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, yarn) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import {
|
||||
buildTree,
|
||||
getChangedPaths,
|
||||
hasChanges,
|
||||
type ChangeType,
|
||||
type TreeNode,
|
||||
} from "../plugins/lib/changed-files-store.js"
|
||||
|
||||
const INDICATORS: Record<ChangeType, string> = {
|
||||
added: "+",
|
||||
modified: "~",
|
||||
deleted: "-",
|
||||
}
|
||||
|
||||
function renderTree(nodes: TreeNode[], indent: string): string {
|
||||
const lines: string[] = []
|
||||
for (const node of nodes) {
|
||||
const indicator = node.changeType ? ` (${INDICATORS[node.changeType]})` : ""
|
||||
const name = node.changeType ? `${node.name}${indicator}` : `${node.name}/`
|
||||
lines.push(`${indent}${name}`)
|
||||
if (node.children.length > 0) {
|
||||
lines.push(renderTree(node.children, `${indent} `))
|
||||
}
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
const changedFilesTool: ToolDefinition = tool({
|
||||
description:
|
||||
"List files changed by agents in this session as a navigable tree. Shows added (+), modified (~), and deleted (-) indicators. Use filter to show only specific change types. Returns paths for git diff.",
|
||||
args: {
|
||||
filter: tool.schema
|
||||
.enum(["all", "added", "modified", "deleted"])
|
||||
.optional()
|
||||
.describe("Filter by change type (default: all)"),
|
||||
format: tool.schema
|
||||
.enum(["tree", "json"])
|
||||
.optional()
|
||||
.describe("Output format: tree for terminal display, json for structured data (default: tree)"),
|
||||
},
|
||||
async execute(args, context) {
|
||||
const filter = args.filter === "all" || !args.filter ? undefined : (args.filter as ChangeType)
|
||||
const format = args.format ?? "tree"
|
||||
|
||||
if (!hasChanges()) {
|
||||
return JSON.stringify({ changed: false, message: "No files changed in this session" })
|
||||
}
|
||||
|
||||
const paths = getChangedPaths(filter)
|
||||
|
||||
if (format === "json") {
|
||||
return JSON.stringify(
|
||||
{
|
||||
changed: true,
|
||||
filter: filter ?? "all",
|
||||
files: paths.map((p) => ({ path: p.path, changeType: p.changeType })),
|
||||
diffCommands: paths
|
||||
.filter((p) => p.changeType !== "added")
|
||||
.map((p) => `git diff ${p.path}`),
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
}
|
||||
|
||||
const tree = buildTree(filter)
|
||||
const treeStr = renderTree(tree, "")
|
||||
const diffHint = paths
|
||||
.filter((p) => p.changeType !== "added")
|
||||
.slice(0, 5)
|
||||
.map((p) => ` git diff ${p.path}`)
|
||||
.join("\n")
|
||||
|
||||
let output = `Changed files (${paths.length}):\n\n${treeStr}`
|
||||
if (diffHint) {
|
||||
output += `\n\nTo view diff for a file:\n${diffHint}`
|
||||
}
|
||||
return output
|
||||
},
|
||||
})
|
||||
|
||||
export default changedFilesTool
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Check Coverage Tool
|
||||
*
|
||||
* Custom OpenCode tool to analyze test coverage and report on gaps.
|
||||
* Supports common coverage report formats.
|
||||
*/
|
||||
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
|
||||
const checkCoverageTool: ToolDefinition = 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 JSON.stringify({
|
||||
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 JSON.stringify(result)
|
||||
},
|
||||
})
|
||||
|
||||
export default checkCoverageTool
|
||||
|
||||
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: [],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* ECC Custom Tool: Dependency Analyzer
|
||||
*
|
||||
* Analyzes project dependencies for outdated packages, security vulnerabilities,
|
||||
* and unused dependencies. Supports multiple package managers.
|
||||
*/
|
||||
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
|
||||
interface DependencyInfo {
|
||||
name: string
|
||||
current: string
|
||||
latest?: string
|
||||
type: "production" | "development" | "peer"
|
||||
outdated: boolean
|
||||
security?: {
|
||||
vulnerable: boolean
|
||||
severity?: string
|
||||
recommendation?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface AnalysisResult {
|
||||
success: boolean
|
||||
packageManager: string
|
||||
dependencies: DependencyInfo[]
|
||||
summary: {
|
||||
total: number
|
||||
outdated: number
|
||||
vulnerable: number
|
||||
unused: number
|
||||
}
|
||||
recommendations: string[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
const dependencyAnalyzerTool: ToolDefinition = tool({
|
||||
description:
|
||||
"Analyze project dependencies for outdated packages, security vulnerabilities, and unused dependencies. Supports npm, pnpm, yarn, and bun.",
|
||||
args: {
|
||||
type: tool.schema
|
||||
.enum(["all", "outdated", "security", "unused"])
|
||||
.optional()
|
||||
.describe("Type of analysis to run (default: all)"),
|
||||
fix: tool.schema
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Attempt to fix issues automatically (default: false)"),
|
||||
depth: tool.schema
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Depth of dependency analysis (default: 1)"),
|
||||
},
|
||||
async execute(args, context): Promise<string> {
|
||||
try {
|
||||
const cwd = context.worktree || context.directory
|
||||
const analysisType = args.type ?? "all"
|
||||
const fix = args.fix ?? false
|
||||
const depth = args.depth ?? 1
|
||||
|
||||
// Detect package manager
|
||||
const packageManager = detectPackageManager(cwd)
|
||||
|
||||
// Analyze dependencies
|
||||
const dependencies = await analyzeDependencies(cwd, packageManager, depth)
|
||||
|
||||
// Generate summary
|
||||
const summary = generateSummary(dependencies)
|
||||
|
||||
// Generate recommendations
|
||||
const recommendations = generateRecommendations(dependencies, summary, analysisType)
|
||||
|
||||
return JSON.stringify({
|
||||
success: true,
|
||||
packageManager,
|
||||
dependencies: dependencies.slice(0, 50), // Limit output
|
||||
summary,
|
||||
recommendations,
|
||||
analysisType,
|
||||
fixMode: fix,
|
||||
platform: process.platform,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: `Failed to analyze dependencies: ${errorMessage}`,
|
||||
type: args.type,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default dependencyAnalyzerTool
|
||||
|
||||
function detectPackageManager(cwd: string): string {
|
||||
if (fs.existsSync(path.join(cwd, "bun.lockb"))) return "bun"
|
||||
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm"
|
||||
if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn"
|
||||
if (fs.existsSync(path.join(cwd, "package-lock.json"))) return "npm"
|
||||
return "npm"
|
||||
}
|
||||
|
||||
async function analyzeDependencies(
|
||||
cwd: string,
|
||||
packageManager: string,
|
||||
depth: number
|
||||
): Promise<DependencyInfo[]> {
|
||||
const dependencies: DependencyInfo[] = []
|
||||
|
||||
try {
|
||||
// Read package.json
|
||||
const packageJsonPath = path.join(cwd, "package.json")
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
throw new Error("package.json not found")
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
|
||||
|
||||
// Analyze production dependencies
|
||||
if (packageJson.dependencies) {
|
||||
for (const [name, version] of Object.entries(packageJson.dependencies)) {
|
||||
dependencies.push({
|
||||
name,
|
||||
current: version as string,
|
||||
type: "production",
|
||||
outdated: false, // Would need npm outdated to check
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze development dependencies
|
||||
if (packageJson.devDependencies) {
|
||||
for (const [name, version] of Object.entries(packageJson.devDependencies)) {
|
||||
dependencies.push({
|
||||
name,
|
||||
current: version as string,
|
||||
type: "development",
|
||||
outdated: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze peer dependencies
|
||||
if (packageJson.peerDependencies) {
|
||||
for (const [name, version] of Object.entries(packageJson.peerDependencies)) {
|
||||
dependencies.push({
|
||||
name,
|
||||
current: version as string,
|
||||
type: "peer",
|
||||
outdated: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read package.json: ${error}`)
|
||||
}
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
function generateSummary(dependencies: DependencyInfo[]) {
|
||||
return {
|
||||
total: dependencies.length,
|
||||
outdated: dependencies.filter(d => d.outdated).length,
|
||||
vulnerable: dependencies.filter(d => d.security?.vulnerable).length,
|
||||
unused: 0, // Would need additional analysis
|
||||
}
|
||||
}
|
||||
|
||||
function generateRecommendations(
|
||||
dependencies: DependencyInfo[],
|
||||
summary: { total: number; outdated: number; vulnerable: number; unused: number },
|
||||
analysisType: string
|
||||
): string[] {
|
||||
const recommendations: string[] = []
|
||||
|
||||
if (summary.outdated > 0) {
|
||||
recommendations.push(
|
||||
`${summary.outdated} outdated dependencies found. Consider updating with: npm update`
|
||||
)
|
||||
}
|
||||
|
||||
if (summary.vulnerable > 0) {
|
||||
recommendations.push(
|
||||
`${summary.vulnerable} vulnerable dependencies found. Run: npm audit fix`
|
||||
)
|
||||
}
|
||||
|
||||
if (summary.total > 100) {
|
||||
recommendations.push(
|
||||
"Large number of dependencies detected. Consider removing unused packages."
|
||||
)
|
||||
}
|
||||
|
||||
// Check for common issues
|
||||
const hasTypeScript = dependencies.some(d => d.name === "typescript")
|
||||
const hasEslint = dependencies.some(d => d.name === "eslint")
|
||||
const hasPrettier = dependencies.some(d => d.name === "prettier")
|
||||
|
||||
if (hasTypeScript && !hasEslint) {
|
||||
recommendations.push(
|
||||
"TypeScript project without ESLint detected. Consider adding linting."
|
||||
)
|
||||
}
|
||||
|
||||
if (hasEslint && !hasPrettier) {
|
||||
recommendations.push(
|
||||
"ESLint without Prettier detected. Consider adding code formatting."
|
||||
)
|
||||
}
|
||||
|
||||
if (recommendations.length === 0) {
|
||||
recommendations.push("No critical dependency issues found.")
|
||||
}
|
||||
|
||||
return recommendations
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* ECC Custom Tool: Format Code
|
||||
*
|
||||
* Returns the formatter command that should be run for a given file.
|
||||
* This avoids shell execution assumptions while still giving precise guidance.
|
||||
* Supports cross-platform command generation.
|
||||
*/
|
||||
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
|
||||
type Formatter = "biome" | "prettier" | "black" | "gofmt" | "rustfmt" | "swift-format"
|
||||
|
||||
interface FormatResult {
|
||||
success: boolean
|
||||
formatter?: Formatter
|
||||
command?: string
|
||||
instructions?: string
|
||||
message?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
const formatCodeTool: ToolDefinition = tool({
|
||||
description:
|
||||
"Detect formatter for a file and return the exact command to run (Biome, Prettier, Black, gofmt, rustfmt, swift-format). Supports cross-platform command generation.",
|
||||
args: {
|
||||
filePath: tool.schema.string().describe("Path to the file to format"),
|
||||
formatter: tool.schema
|
||||
.enum(["biome", "prettier", "black", "gofmt", "rustfmt", "swift-format"])
|
||||
.optional()
|
||||
.describe("Optional formatter override"),
|
||||
},
|
||||
async execute(args, context): Promise<string> {
|
||||
try {
|
||||
const cwd = context.worktree || context.directory
|
||||
const ext = args.filePath.split(".").pop()?.toLowerCase() || ""
|
||||
const detected = args.formatter || detectFormatter(cwd, ext)
|
||||
|
||||
if (!detected) {
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
message: `No formatter detected for .${ext} files`,
|
||||
supportedFormatters: ["biome", "prettier", "black", "gofmt", "rustfmt", "swift-format"],
|
||||
})
|
||||
}
|
||||
|
||||
const command = buildFormatterCommand(detected, args.filePath, cwd)
|
||||
return JSON.stringify({
|
||||
success: true,
|
||||
formatter: detected,
|
||||
command,
|
||||
instructions: `Run this command:\n\n${command}`,
|
||||
platform: process.platform,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: `Failed to detect formatter: ${errorMessage}`,
|
||||
filePath: args.filePath,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default formatCodeTool
|
||||
|
||||
function detectFormatter(cwd: string, ext: string): Formatter | null {
|
||||
// Check for formatter config files
|
||||
const hasConfig = (configFiles: string[]): boolean => {
|
||||
return configFiles.some(configFile => fs.existsSync(path.join(cwd, configFile)))
|
||||
}
|
||||
|
||||
// JavaScript/TypeScript files
|
||||
if (["ts", "tsx", "js", "jsx", "json", "css", "scss", "md", "yaml", "yml"].includes(ext)) {
|
||||
if (hasConfig(["biome.json", "biome.jsonc"])) {
|
||||
return "biome"
|
||||
}
|
||||
return "prettier"
|
||||
}
|
||||
|
||||
// Python files
|
||||
if (["py", "pyi"].includes(ext)) {
|
||||
return "black"
|
||||
}
|
||||
|
||||
// Go files
|
||||
if (ext === "go") {
|
||||
return "gofmt"
|
||||
}
|
||||
|
||||
// Rust files
|
||||
if (ext === "rs") {
|
||||
return "rustfmt"
|
||||
}
|
||||
|
||||
// Swift files
|
||||
if (ext === "swift") {
|
||||
return "swift-format"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function buildFormatterCommand(formatter: Formatter, filePath: string, cwd?: string): string {
|
||||
// Normalize to forward slashes so the emitted command is identical on every
|
||||
// platform. `path.normalize` yields backslashes on Windows, which broke the
|
||||
// command string (and Windows CI); all formatter CLIs accept `/` on Windows.
|
||||
const normalizedPath = path.normalize(filePath).split(path.sep).join("/")
|
||||
|
||||
// Build command based on formatter and platform
|
||||
const commands: Record<Formatter, string> = {
|
||||
biome: `npx @biomejs/biome format --write ${normalizedPath}`,
|
||||
prettier: `npx prettier --write ${normalizedPath}`,
|
||||
black: `black ${normalizedPath}`,
|
||||
gofmt: `gofmt -w ${normalizedPath}`,
|
||||
rustfmt: `rustfmt ${normalizedPath}`,
|
||||
"swift-format": `swift-format format --in-place ${normalizedPath}`,
|
||||
}
|
||||
|
||||
return commands[formatter]
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* ECC Custom Tool: Git Summary
|
||||
*
|
||||
* Returns branch/status/log/diff details for the active repository.
|
||||
*/
|
||||
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import { execFileSync } from "child_process"
|
||||
|
||||
// Conservative subset of git's allowed ref-name characters. Rejects shell
|
||||
// metacharacters and option-like leading `-` so a model-supplied baseBranch
|
||||
// cannot inject into the shell command line built below.
|
||||
const SAFE_GIT_REF = /^[A-Za-z0-9._/-]+$/
|
||||
|
||||
function isSafeRef(ref: string): boolean {
|
||||
if (typeof ref !== "string" || ref.length === 0 || ref.length > 200) return false
|
||||
if (!SAFE_GIT_REF.test(ref)) return false
|
||||
if (ref.startsWith("-") || ref.startsWith(".") || ref.startsWith("/")) return false
|
||||
if (ref.includes("..") || ref.includes("//")) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function isSafeDepth(value: unknown): value is number {
|
||||
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 1000
|
||||
}
|
||||
|
||||
const gitSummaryTool: ToolDefinition = tool({
|
||||
description:
|
||||
"Generate git summary with branch, status, recent commits, and optional diff stats.",
|
||||
args: {
|
||||
depth: tool.schema
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Number of recent commits to include (default: 5)"),
|
||||
includeDiff: tool.schema
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Include diff stats against base branch (default: true)"),
|
||||
baseBranch: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Base branch for diff comparison (default: main)"),
|
||||
},
|
||||
async execute(args, context) {
|
||||
const cwd = context.worktree || context.directory
|
||||
const depth = isSafeDepth(args.depth) ? args.depth : 5
|
||||
const includeDiff = args.includeDiff ?? true
|
||||
const baseBranch = args.baseBranch ?? "main"
|
||||
|
||||
const result: Record<string, string> = {
|
||||
branch: runArgs(["branch", "--show-current"], cwd) || "unknown",
|
||||
status: runArgs(["status", "--short"], cwd) || "clean",
|
||||
log: runArgs(["log", "--oneline", `-${depth}`], cwd) || "no commits found",
|
||||
}
|
||||
|
||||
if (includeDiff) {
|
||||
result.stagedDiff = runArgs(["diff", "--cached", "--stat"], cwd) || ""
|
||||
result.branchDiff = isSafeRef(baseBranch)
|
||||
? runArgs(["diff", `${baseBranch}...HEAD`, "--stat"], cwd) ||
|
||||
`unable to diff against ${baseBranch}`
|
||||
: `unable to diff against ${baseBranch} (invalid ref)`
|
||||
}
|
||||
|
||||
return JSON.stringify(result)
|
||||
},
|
||||
})
|
||||
|
||||
export default gitSummaryTool
|
||||
|
||||
function runArgs(args: string[], cwd: string): string {
|
||||
try {
|
||||
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim()
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* ECC Custom Tools for OpenCode
|
||||
*
|
||||
* These tools extend OpenCode with additional capabilities.
|
||||
*/
|
||||
|
||||
// Re-export all tools
|
||||
export { default as runTests } from "./run-tests.js"
|
||||
export { default as checkCoverage } from "./check-coverage.js"
|
||||
export { default as securityAudit } from "./security-audit.js"
|
||||
export { default as formatCode } from "./format-code.js"
|
||||
export { default as lintCheck } from "./lint-check.js"
|
||||
export { default as gitSummary } from "./git-summary.js"
|
||||
export { default as changedFiles } from "./changed-files.js"
|
||||
export { default as dependencyAnalyzer } from "./dependency-analyzer.js"
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* ECC Custom Tool: Lint Check
|
||||
*
|
||||
* Detects the appropriate linter and returns a runnable lint command.
|
||||
* Supports cross-platform command generation and error handling.
|
||||
*/
|
||||
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
|
||||
type Linter = "biome" | "eslint" | "ruff" | "pylint" | "golangci-lint"
|
||||
|
||||
interface LintResult {
|
||||
success: boolean
|
||||
linter?: Linter
|
||||
command?: string
|
||||
instructions?: string
|
||||
message?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
const lintCheckTool: ToolDefinition = tool({
|
||||
description:
|
||||
"Detect linter for a target path and return command for check/fix runs. Supports cross-platform command generation.",
|
||||
args: {
|
||||
target: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.describe("File or directory to lint (default: current directory)"),
|
||||
fix: tool.schema
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Enable auto-fix mode"),
|
||||
linter: tool.schema
|
||||
.enum(["biome", "eslint", "ruff", "pylint", "golangci-lint"])
|
||||
.optional()
|
||||
.describe("Optional linter override"),
|
||||
},
|
||||
async execute(args, context): Promise<string> {
|
||||
try {
|
||||
const cwd = context.worktree || context.directory
|
||||
const target = args.target || "."
|
||||
const fix = args.fix ?? false
|
||||
const detected = args.linter || detectLinter(cwd)
|
||||
|
||||
const command = buildLintCommand(detected, target, fix)
|
||||
return JSON.stringify({
|
||||
success: true,
|
||||
linter: detected,
|
||||
command,
|
||||
instructions: `Run this command:\n\n${command}`,
|
||||
platform: process.platform,
|
||||
fixMode: fix,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return JSON.stringify({
|
||||
success: false,
|
||||
error: `Failed to detect linter: ${errorMessage}`,
|
||||
target: args.target,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default lintCheckTool
|
||||
|
||||
function detectLinter(cwd: string): Linter {
|
||||
// Check for Biome config
|
||||
if (fs.existsSync(path.join(cwd, "biome.json")) || fs.existsSync(path.join(cwd, "biome.jsonc"))) {
|
||||
return "biome"
|
||||
}
|
||||
|
||||
// Check for ESLint config
|
||||
const eslintConfigs = [
|
||||
".eslintrc.json",
|
||||
".eslintrc.js",
|
||||
".eslintrc.cjs",
|
||||
"eslint.config.js",
|
||||
"eslint.config.mjs",
|
||||
]
|
||||
if (eslintConfigs.some((name) => fs.existsSync(path.join(cwd, name)))) {
|
||||
return "eslint"
|
||||
}
|
||||
|
||||
// Check for Python linters
|
||||
const pyprojectPath = path.join(cwd, "pyproject.toml")
|
||||
if (fs.existsSync(pyprojectPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(pyprojectPath, "utf-8")
|
||||
if (content.includes("ruff")) return "ruff"
|
||||
if (content.includes("pylint")) return "pylint"
|
||||
} catch {
|
||||
// ignore read errors and keep fallback logic
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Go linter
|
||||
if (fs.existsSync(path.join(cwd, ".golangci.yml")) || fs.existsSync(path.join(cwd, ".golangci.yaml"))) {
|
||||
return "golangci-lint"
|
||||
}
|
||||
|
||||
// Default to ESLint for JavaScript/TypeScript projects
|
||||
return "eslint"
|
||||
}
|
||||
|
||||
function buildLintCommand(linter: Linter, target: string, fix: boolean): string {
|
||||
// Normalize target path for cross-platform compatibility
|
||||
const normalizedTarget = path.normalize(target)
|
||||
|
||||
// Build command based on linter and platform
|
||||
const commands: Record<Linter, string> = {
|
||||
biome: `npx @biomejs/biome lint${fix ? " --write" : ""} ${normalizedTarget}`,
|
||||
eslint: `npx eslint${fix ? " --fix" : ""} ${normalizedTarget}`,
|
||||
ruff: `ruff check${fix ? " --fix" : ""} ${normalizedTarget}`,
|
||||
pylint: `pylint ${normalizedTarget}`,
|
||||
"golangci-lint": `golangci-lint run ${normalizedTarget}`,
|
||||
}
|
||||
|
||||
return commands[linter]
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Run Tests Tool
|
||||
*
|
||||
* Custom OpenCode tool to run test suites with various options.
|
||||
* Automatically detects the package manager and test framework.
|
||||
*/
|
||||
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
|
||||
const runTestsTool: ToolDefinition = tool({
|
||||
description:
|
||||
"Run the test suite with optional coverage, watch mode, or specific test patterns. Automatically detects package manager (npm, pnpm, yarn, bun) and test framework.",
|
||||
args: {
|
||||
pattern: tool.schema
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Test file pattern or specific test name to run"),
|
||||
coverage: tool.schema
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Run with coverage reporting (default: false)"),
|
||||
watch: tool.schema
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Run in watch mode for continuous testing (default: false)"),
|
||||
updateSnapshots: tool.schema
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Update Jest/Vitest snapshots (default: false)"),
|
||||
},
|
||||
async execute(args, context) {
|
||||
const { pattern, coverage, watch, updateSnapshots } = args
|
||||
const cwd = context.worktree || context.directory
|
||||
|
||||
// Detect package manager
|
||||
const packageManager = await detectPackageManager(cwd)
|
||||
|
||||
// Detect test framework
|
||||
const testFramework = await detectTestFramework(cwd)
|
||||
|
||||
// Build command
|
||||
let cmd: string[] = [packageManager]
|
||||
|
||||
if (packageManager === "npm") {
|
||||
cmd.push("run", "test")
|
||||
} else {
|
||||
cmd.push("test")
|
||||
}
|
||||
|
||||
// Add options based on framework
|
||||
const testArgs: string[] = []
|
||||
|
||||
if (coverage) {
|
||||
testArgs.push("--coverage")
|
||||
}
|
||||
|
||||
if (watch) {
|
||||
testArgs.push("--watch")
|
||||
}
|
||||
|
||||
if (updateSnapshots) {
|
||||
testArgs.push("-u")
|
||||
}
|
||||
|
||||
if (pattern) {
|
||||
if (testFramework === "jest" || testFramework === "vitest") {
|
||||
testArgs.push("--testPathPattern", pattern)
|
||||
} else {
|
||||
testArgs.push(pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// Add -- separator for npm
|
||||
if (testArgs.length > 0) {
|
||||
if (packageManager === "npm") {
|
||||
cmd.push("--")
|
||||
}
|
||||
cmd.push(...testArgs)
|
||||
}
|
||||
|
||||
const command = cmd.join(" ")
|
||||
|
||||
return JSON.stringify({
|
||||
command,
|
||||
packageManager,
|
||||
testFramework,
|
||||
options: {
|
||||
pattern: pattern || "all tests",
|
||||
coverage: coverage || false,
|
||||
watch: watch || false,
|
||||
updateSnapshots: updateSnapshots || false,
|
||||
},
|
||||
instructions: `Run this command to execute tests:\n\n${command}`,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export default runTestsTool
|
||||
|
||||
async function detectPackageManager(cwd: string): Promise<string> {
|
||||
const lockFiles: Record<string, string> = {
|
||||
"bun.lockb": "bun",
|
||||
"pnpm-lock.yaml": "pnpm",
|
||||
"yarn.lock": "yarn",
|
||||
"package-lock.json": "npm",
|
||||
}
|
||||
|
||||
for (const [lockFile, pm] of Object.entries(lockFiles)) {
|
||||
if (fs.existsSync(path.join(cwd, lockFile))) {
|
||||
return pm
|
||||
}
|
||||
}
|
||||
|
||||
return "npm"
|
||||
}
|
||||
|
||||
async function detectTestFramework(cwd: string): Promise<string> {
|
||||
const packageJsonPath = path.join(cwd, "package.json")
|
||||
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
|
||||
const deps = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies,
|
||||
}
|
||||
|
||||
if (deps.vitest) return "vitest"
|
||||
if (deps.jest) return "jest"
|
||||
if (deps.mocha) return "mocha"
|
||||
if (deps.ava) return "ava"
|
||||
if (deps.tap) return "tap"
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* Security Audit Tool
|
||||
*
|
||||
* Custom OpenCode tool to run security audits on dependencies and code.
|
||||
* Combines npm audit, secret scanning, and OWASP checks.
|
||||
*
|
||||
* NOTE: This tool SCANS for security anti-patterns - it does not introduce them.
|
||||
* The regex patterns below are used to DETECT potential issues in user code.
|
||||
*/
|
||||
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
|
||||
const securityAuditTool: ToolDefinition = tool({
|
||||
description:
|
||||
"Run a comprehensive security audit including dependency vulnerabilities, secret scanning, and common security issues.",
|
||||
args: {
|
||||
type: tool.schema
|
||||
.enum(["all", "dependencies", "secrets", "code"])
|
||||
.optional()
|
||||
.describe("Type of audit to run (default: all)"),
|
||||
fix: tool.schema
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Attempt to auto-fix dependency vulnerabilities (default: false)"),
|
||||
severity: tool.schema
|
||||
.enum(["low", "moderate", "high", "critical"])
|
||||
.optional()
|
||||
.describe("Minimum severity level to report (default: moderate)"),
|
||||
},
|
||||
async execute(args, context) {
|
||||
const auditType = args.type ?? "all"
|
||||
const fix = args.fix ?? false
|
||||
const severity = args.severity ?? "moderate"
|
||||
const cwd = context.worktree || context.directory
|
||||
|
||||
const results: AuditResults = {
|
||||
timestamp: new Date().toISOString(),
|
||||
directory: cwd,
|
||||
checks: [],
|
||||
summary: {
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
warnings: 0,
|
||||
},
|
||||
}
|
||||
|
||||
// Check for dependencies audit
|
||||
if (auditType === "all" || auditType === "dependencies") {
|
||||
results.checks.push({
|
||||
name: "Dependency Vulnerabilities",
|
||||
description: "Check for known vulnerabilities in dependencies",
|
||||
command: fix ? "npm audit fix" : "npm audit",
|
||||
severityFilter: severity,
|
||||
status: "pending",
|
||||
})
|
||||
}
|
||||
|
||||
// Check for secrets
|
||||
if (auditType === "all" || auditType === "secrets") {
|
||||
const secretPatterns = await scanForSecrets(cwd)
|
||||
if (secretPatterns.length > 0) {
|
||||
results.checks.push({
|
||||
name: "Secret Detection",
|
||||
description: "Scan for hardcoded secrets and API keys",
|
||||
status: "failed",
|
||||
findings: secretPatterns,
|
||||
})
|
||||
results.summary.failed++
|
||||
} else {
|
||||
results.checks.push({
|
||||
name: "Secret Detection",
|
||||
description: "Scan for hardcoded secrets and API keys",
|
||||
status: "passed",
|
||||
})
|
||||
results.summary.passed++
|
||||
}
|
||||
}
|
||||
|
||||
// Check for common code security issues
|
||||
if (auditType === "all" || auditType === "code") {
|
||||
const codeIssues = await scanCodeSecurity(cwd)
|
||||
if (codeIssues.length > 0) {
|
||||
results.checks.push({
|
||||
name: "Code Security",
|
||||
description: "Check for common security anti-patterns",
|
||||
status: "warning",
|
||||
findings: codeIssues,
|
||||
})
|
||||
results.summary.warnings++
|
||||
} else {
|
||||
results.checks.push({
|
||||
name: "Code Security",
|
||||
description: "Check for common security anti-patterns",
|
||||
status: "passed",
|
||||
})
|
||||
results.summary.passed++
|
||||
}
|
||||
}
|
||||
|
||||
// Generate recommendations
|
||||
results.recommendations = generateRecommendations(results)
|
||||
|
||||
return JSON.stringify(results)
|
||||
},
|
||||
})
|
||||
|
||||
export default securityAuditTool
|
||||
|
||||
interface AuditCheck {
|
||||
name: string
|
||||
description: string
|
||||
command?: string
|
||||
severityFilter?: string
|
||||
status: "pending" | "passed" | "failed" | "warning"
|
||||
findings?: Array<{ file: string; issue: string; line?: number }>
|
||||
}
|
||||
|
||||
interface AuditResults {
|
||||
timestamp: string
|
||||
directory: string
|
||||
checks: AuditCheck[]
|
||||
summary: {
|
||||
passed: number
|
||||
failed: number
|
||||
warnings: number
|
||||
}
|
||||
recommendations?: string[]
|
||||
}
|
||||
|
||||
async function scanForSecrets(
|
||||
cwd: string
|
||||
): Promise<Array<{ file: string; issue: string; line?: number }>> {
|
||||
const findings: Array<{ file: string; issue: string; line?: number }> = []
|
||||
|
||||
// Patterns to DETECT potential secrets (security scanning)
|
||||
const secretPatterns = [
|
||||
{ pattern: /api[_-]?key\s*[:=]\s*['"][^'"]{20,}['"]/gi, name: "API Key" },
|
||||
{ pattern: /password\s*[:=]\s*['"][^'"]+['"]/gi, name: "Password" },
|
||||
{ pattern: /secret\s*[:=]\s*['"][^'"]{10,}['"]/gi, name: "Secret" },
|
||||
{ pattern: /Bearer\s+[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/g, name: "JWT Token" },
|
||||
{ pattern: /sk-[a-zA-Z0-9]{32,}/g, name: "OpenAI API Key" },
|
||||
{ pattern: /ghp_[a-zA-Z0-9]{36}/g, name: "GitHub Token" },
|
||||
{ pattern: /aws[_-]?secret[_-]?access[_-]?key/gi, name: "AWS Secret" },
|
||||
]
|
||||
|
||||
const ignorePatterns = [
|
||||
"node_modules",
|
||||
".git",
|
||||
"dist",
|
||||
"build",
|
||||
".env.example",
|
||||
".env.template",
|
||||
]
|
||||
|
||||
const srcDir = path.join(cwd, "src")
|
||||
if (fs.existsSync(srcDir)) {
|
||||
await scanDirectory(srcDir, secretPatterns, ignorePatterns, findings)
|
||||
}
|
||||
|
||||
// Also check root config files
|
||||
const configFiles = ["config.js", "config.ts", "settings.js", "settings.ts"]
|
||||
for (const configFile of configFiles) {
|
||||
const filePath = path.join(cwd, configFile)
|
||||
if (fs.existsSync(filePath)) {
|
||||
await scanFile(filePath, secretPatterns, findings)
|
||||
}
|
||||
}
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
async function scanDirectory(
|
||||
dir: string,
|
||||
patterns: Array<{ pattern: RegExp; name: string }>,
|
||||
ignorePatterns: string[],
|
||||
findings: Array<{ file: string; issue: string; line?: number }>
|
||||
): Promise<void> {
|
||||
if (!fs.existsSync(dir)) return
|
||||
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
|
||||
if (ignorePatterns.some((p) => fullPath.includes(p))) continue
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await scanDirectory(fullPath, patterns, ignorePatterns, findings)
|
||||
} else if (entry.isFile() && entry.name.match(/\.(ts|tsx|js|jsx|json)$/)) {
|
||||
await scanFile(fullPath, patterns, findings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function scanFile(
|
||||
filePath: string,
|
||||
patterns: Array<{ pattern: RegExp; name: string }>,
|
||||
findings: Array<{ file: string; issue: string; line?: number }>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, "utf-8")
|
||||
const lines = content.split("\n")
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
for (const { pattern, name } of patterns) {
|
||||
// Reset regex state
|
||||
pattern.lastIndex = 0
|
||||
if (pattern.test(line)) {
|
||||
findings.push({
|
||||
file: filePath,
|
||||
issue: `Potential ${name} found`,
|
||||
line: i + 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore read errors
|
||||
}
|
||||
}
|
||||
|
||||
async function scanCodeSecurity(
|
||||
cwd: string
|
||||
): Promise<Array<{ file: string; issue: string; line?: number }>> {
|
||||
const findings: Array<{ file: string; issue: string; line?: number }> = []
|
||||
|
||||
// Patterns to DETECT security anti-patterns (this tool scans for issues)
|
||||
// These are detection patterns, not code that uses these anti-patterns
|
||||
const securityPatterns = [
|
||||
{ pattern: /\beval\s*\(/g, name: "eval() usage - potential code injection" },
|
||||
{ pattern: /innerHTML\s*=/g, name: "innerHTML assignment - potential XSS" },
|
||||
{ pattern: /dangerouslySetInnerHTML/g, name: "dangerouslySetInnerHTML - potential XSS" },
|
||||
{ pattern: /document\.write/g, name: "document.write - potential XSS" },
|
||||
{ pattern: /\$\{.*\}.*sql/gi, name: "Potential SQL injection" },
|
||||
]
|
||||
|
||||
const srcDir = path.join(cwd, "src")
|
||||
if (fs.existsSync(srcDir)) {
|
||||
await scanDirectory(srcDir, securityPatterns, ["node_modules", ".git", "dist"], findings)
|
||||
}
|
||||
|
||||
return findings
|
||||
}
|
||||
|
||||
function generateRecommendations(results: AuditResults): string[] {
|
||||
const recommendations: string[] = []
|
||||
|
||||
for (const check of results.checks) {
|
||||
if (check.status === "failed" && check.name === "Secret Detection") {
|
||||
recommendations.push(
|
||||
"CRITICAL: Remove hardcoded secrets and use environment variables instead"
|
||||
)
|
||||
recommendations.push("Add a .env file (gitignored) for local development")
|
||||
recommendations.push("Use a secrets manager for production deployments")
|
||||
}
|
||||
|
||||
if (check.status === "warning" && check.name === "Code Security") {
|
||||
recommendations.push(
|
||||
"Review flagged code patterns for potential security vulnerabilities"
|
||||
)
|
||||
recommendations.push("Consider using DOMPurify for HTML sanitization")
|
||||
recommendations.push("Use parameterized queries for database operations")
|
||||
}
|
||||
|
||||
if (check.status === "pending" && check.name === "Dependency Vulnerabilities") {
|
||||
recommendations.push("Run 'npm audit' to check for dependency vulnerabilities")
|
||||
recommendations.push("Consider using 'npm audit fix' to auto-fix issues")
|
||||
}
|
||||
}
|
||||
|
||||
if (recommendations.length === 0) {
|
||||
recommendations.push("No critical security issues found. Continue following security best practices.")
|
||||
}
|
||||
|
||||
return recommendations
|
||||
}
|
||||
Reference in New Issue
Block a user