206 lines
6.6 KiB
TypeScript
206 lines
6.6 KiB
TypeScript
import { parametersSchema as z, defineCustomTool, CustomToolContext } from "@roo-code/types"
|
|
// @ts-ignore - Node built-ins
|
|
import { spawnSync } from "child_process"
|
|
// @ts-ignore - Node built-ins
|
|
import { readFileSync, unlinkSync, existsSync } from "fs"
|
|
// @ts-ignore - Node built-ins
|
|
import path from "path"
|
|
// @ts-ignore - Node built-ins
|
|
import os from "os"
|
|
|
|
interface DepNode {
|
|
groupId: string
|
|
artifactId: string
|
|
version: string
|
|
scope: string
|
|
depth: number
|
|
children: DepNode[]
|
|
}
|
|
|
|
export default defineCustomTool({
|
|
name: "mvn_dependency_tree",
|
|
description: "Run 'mvn dependency:tree' and return parsed JSON. Filters by groupId and scope. Saves piping raw mvn output through grep.",
|
|
parameters: z.object({
|
|
projectRoot: z.string().describe("Directory containing pom.xml (absolute or relative)"),
|
|
module: z.string().optional().describe("-pl <module> scope, e.g. 'backend' or 'java/modules/cs-modules/eau'"),
|
|
groupIdFilter: z.string().optional().describe("Only include deps whose groupId contains this string"),
|
|
scope: z.enum(["compile", "test", "runtime", "provided", "all"]).optional().describe("Filter by Maven scope (default: all)"),
|
|
}),
|
|
async execute({ projectRoot, module, groupIdFilter, scope = "all" }, context: CustomToolContext) {
|
|
try {
|
|
// Resolve projectRoot against task CWD if relative
|
|
// @ts-ignore - task.cwd exists at runtime
|
|
const cwd = context?.task?.cwd ?? process.cwd()
|
|
const resolvedRoot = path.isAbsolute(projectRoot) ? projectRoot : path.resolve(cwd, projectRoot)
|
|
|
|
// Use a temp file for output — avoids parsing noisy Maven stdout
|
|
const outFile = path.join(os.tmpdir(), `mvn-deptree-${Date.now()}.txt`)
|
|
|
|
const args = ["dependency:tree", "-B", `-DoutputFile=${outFile}`, "-DoutputType=text"]
|
|
// Only add -pl when module is a non-empty string
|
|
if (module && module.trim()) {
|
|
args.push("-pl", module, "-am")
|
|
}
|
|
if (scope !== "all") {
|
|
args.push(`-Dscope=${scope}`)
|
|
}
|
|
|
|
const result = spawnSync("mvn", args, {
|
|
cwd: resolvedRoot,
|
|
encoding: "utf-8",
|
|
timeout: 120_000,
|
|
maxBuffer: 10 * 1024 * 1024,
|
|
})
|
|
|
|
if (result.error) {
|
|
return JSON.stringify({ error: `spawn error: ${result.error.message}` }, null, 2)
|
|
}
|
|
|
|
const stderr = result.stderr || ""
|
|
|
|
if (result.status !== 0) {
|
|
const errLines = stderr.split("\n").filter((l: string) => l.includes("[ERROR]")).slice(0, 10)
|
|
// Clean up temp file if it exists
|
|
try { if (existsSync(outFile)) unlinkSync(outFile) } catch {}
|
|
return JSON.stringify({
|
|
ok: false,
|
|
command: `mvn ${args.join(" ")}`,
|
|
exitCode: result.status,
|
|
errorLines: errLines,
|
|
}, null, 2)
|
|
}
|
|
|
|
// Read the output file
|
|
let output = ""
|
|
try {
|
|
output = readFileSync(outFile, "utf-8")
|
|
} catch (e: any) {
|
|
return JSON.stringify({
|
|
error: `mvn succeeded but output file not found: ${outFile}. stderr: ${stderr.slice(0, 500)}`,
|
|
}, null, 2)
|
|
} finally {
|
|
// Clean up temp file
|
|
try { if (existsSync(outFile)) unlinkSync(outFile) } catch {}
|
|
}
|
|
|
|
// Parse the tree output
|
|
// Lines look like:
|
|
// de.platesoft:inspectflow:pom:0.1.0
|
|
// +- org.springframework.boot:spring-boot-starter-web:jar:3.5.11:compile
|
|
// | +- org.springframework.boot:spring-boot-starter:jar:3.5.11:compile
|
|
// \- junit:junit:jar:4.13.2:test
|
|
const lines = output.split("\n").filter((l: string) => l.trim())
|
|
|
|
let rootArtifact = ""
|
|
const tree: DepNode[] = []
|
|
const stack: { node: DepNode; depth: number }[] = []
|
|
let totalCount = 0
|
|
|
|
for (const line of lines) {
|
|
// Determine depth by prefix characters
|
|
// Root line has no prefix markers
|
|
const trimmed = line.replace(/^[\s|\\+\-]+/, "").trim()
|
|
if (!trimmed || trimmed.startsWith("[")) continue
|
|
|
|
// Calculate depth from prefix
|
|
let depth = 0
|
|
const prefixMatch = line.match(/^([\s|\\+\-]*)/)
|
|
if (prefixMatch) {
|
|
const prefix = prefixMatch[1]
|
|
// Each level is 3 chars: "+- " or "| " or "\- "
|
|
if (prefix.length === 0) {
|
|
depth = 0
|
|
} else {
|
|
depth = Math.ceil(prefix.length / 3)
|
|
}
|
|
}
|
|
|
|
// Parse: groupId:artifactId:packaging:version:scope
|
|
// or: groupId:artifactId:packaging:classifier:version:scope
|
|
const parts = trimmed.split(":")
|
|
if (parts.length < 4) {
|
|
if (depth === 0 && parts.length >= 3) {
|
|
rootArtifact = trimmed
|
|
}
|
|
continue
|
|
}
|
|
|
|
let gId: string, aId: string, ver: string, sc: string
|
|
if (parts.length === 5) {
|
|
[gId, aId, , ver, sc] = parts
|
|
} else if (parts.length >= 6) {
|
|
[gId, aId, , , ver, sc] = parts
|
|
} else {
|
|
[gId, aId, , ver] = parts
|
|
sc = "compile"
|
|
}
|
|
|
|
if (depth === 0 && !rootArtifact) {
|
|
rootArtifact = trimmed
|
|
continue
|
|
}
|
|
|
|
totalCount++
|
|
|
|
// Apply filters
|
|
if (groupIdFilter && !gId.includes(groupIdFilter)) continue
|
|
if (scope !== "all" && sc !== scope) continue
|
|
|
|
const node: DepNode = {
|
|
groupId: gId,
|
|
artifactId: aId,
|
|
version: ver,
|
|
scope: sc,
|
|
depth,
|
|
children: [],
|
|
}
|
|
|
|
// Place in tree
|
|
while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
|
|
stack.pop()
|
|
}
|
|
|
|
if (stack.length === 0) {
|
|
tree.push(node)
|
|
} else {
|
|
stack[stack.length - 1].node.children.push(node)
|
|
}
|
|
stack.push({ node, depth })
|
|
}
|
|
|
|
// Count filtered nodes
|
|
const countNodes = (nodes: DepNode[]): number => {
|
|
let c = nodes.length
|
|
for (const n of nodes) c += countNodes(n.children)
|
|
return c
|
|
}
|
|
const filtered = countNodes(tree)
|
|
|
|
// Truncate if too large
|
|
let truncated = false
|
|
const MAX_NODES = 500
|
|
if (filtered > MAX_NODES) {
|
|
truncated = true
|
|
// Flatten to first 2 levels only
|
|
for (const node of tree) {
|
|
for (const child of node.children) {
|
|
child.children = []
|
|
}
|
|
}
|
|
}
|
|
|
|
return JSON.stringify({
|
|
command: `mvn ${args.join(" ")}`,
|
|
ok: true,
|
|
rootArtifact,
|
|
dependencyCount: totalCount,
|
|
filtered,
|
|
truncated,
|
|
tree,
|
|
}, null, 2)
|
|
} catch (err: any) {
|
|
return JSON.stringify({ error: err.message ?? String(err) }, null, 2)
|
|
}
|
|
},
|
|
})
|