Files
pi_mcps/zoo_backup/home/tools/mvn_dependency_tree.ts
T
2026-06-24 19:27:14 +02:00

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)
}
},
})