feat: archive zoo_backup for home sync
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
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)
|
||||
}
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user