360 Secure SkillHub
360 安全技能中心

Monorepo Analyzer

核心功能:检测npm/yarn等多类工作区工具,映射包间依赖,排查循环依赖、版本不一致、未使用包问题,计算拓扑构建顺序,生成统计数据。典型场景:分析monorepo结构、入职大型项目、健康检查等。无需安装依赖,仅通过配置文件分析,输出支持文本、JSON、Markdown、Mermaid等格式,可集成CI流程。

charlie-morrison
charlie-morrison
热度 47.42
最近更新 2026-05-11 17:46:16
分类:开发依赖:代码执行 / 读写文件应用场景:DevOps / 数据处理 / 调试排错 / 报告撰写 ...

安装技能

复制下面的话术发给你的 Agent,它会通过 secure-skills 检索并安装该技能。

执行命令安装此 skill: npx secure-skills-cli add monorepo-analyzer
Metadata
name
monorepo-analyzer
description
Analyze monorepo structure — detect workspace tools (npm/yarn/pnpm/lerna/nx/turbo/cargo/go), map inter-package dependencies, find unused packages, detect version inconsistencies, compute build order, and identify circular dependencies.

Monorepo Analyzer

Understand and audit monorepo structure. Detects workspace configuration, maps package dependencies, finds problems (circular deps, version mismatches, unused packages), and computes optimal build order.

Use when: "analyze this monorepo", "show package dependencies", "find unused packages", "check for circular deps", "what's the build order", "monorepo health check", or when onboarding to a large monorepo.

Commands

1. discover — Detect Monorepo Configuration

Identify the workspace tool and enumerate all packages.

# Detect workspace tool
echo "Checking workspace configuration..."

# npm workspaces (package.json)
if [ -f "package.json" ]; then
  python3 -c "
import json, glob
d = json.load(open('package.json'))
ws = d.get('workspaces', [])
if isinstance(ws, dict): ws = ws.get('packages', [])
if ws:
    print('Tool: npm workspaces')
    print(f'Workspace globs: {ws}')
    for pattern in ws:
        for p in glob.glob(pattern + '/package.json'):
            name = json.load(open(p)).get('name', p)
            print(f'  Package: {name} ({p})')
" 2>/dev/null
fi

# pnpm workspaces
if [ -f "pnpm-workspace.yaml" ]; then
  echo "Tool: pnpm workspaces"
  cat pnpm-workspace.yaml
fi

# Yarn workspaces (package.json or .yarnrc.yml)
if [ -f ".yarnrc.yml" ]; then
  echo "Tool: Yarn (Berry)"
  grep "nodeLinker" .yarnrc.yml 2>/dev/null
fi

# Lerna
if [ -f "lerna.json" ]; then
  echo "Tool: Lerna"
  cat lerna.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'Version: {d.get(\"version\")}, Packages: {d.get(\"packages\")}')" 2>/dev/null
fi

# Nx
if [ -f "nx.json" ]; then
  echo "Tool: Nx"
  cat nx.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'Affected default base: {d.get(\"affected\",{}).get(\"defaultBase\",\"main\")}')" 2>/dev/null
fi

# Turborepo
if [ -f "turbo.json" ]; then
  echo "Tool: Turborepo"
  cat turbo.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'Pipeline tasks: {list(d.get(\"pipeline\",d.get(\"tasks\",{})).keys())}')" 2>/dev/null
fi

# Cargo workspaces (Rust)
if [ -f "Cargo.toml" ]; then
  grep -A20 "\[workspace\]" Cargo.toml 2>/dev/null && echo "Tool: Cargo workspace"
fi

# Go workspaces
if [ -f "go.work" ]; then
  echo "Tool: Go workspace"
  cat go.work
fi

Output: workspace tool, total package count, package names and paths.

2. deps — Inter-Package Dependency Graph

Map which packages depend on which other packages within the monorepo.

# For JS/TS monorepos: extract internal dependencies
python3 -c "
import json, glob, os

# Collect all package names
packages = {}
for pj in glob.glob('**/package.json', recursive=True):
    if 'node_modules' in pj: continue
    try:
        d = json.load(open(pj))
        name = d.get('name')
        if name:
            packages[name] = {
                'path': os.path.dirname(pj),
                'deps': list(d.get('dependencies', {}).keys()),
                'devDeps': list(d.get('devDependencies', {}).keys()),
                'peerDeps': list(d.get('peerDependencies', {}).keys())
            }
    except: pass

# Filter to internal deps only
internal_names = set(packages.keys())
print(f'Total packages: {len(packages)}')
print()
for name, info in sorted(packages.items()):
    internal_deps = [d for d in info['deps'] if d in internal_names]
    internal_dev = [d for d in info['devDeps'] if d in internal_names]
    internal_peer = [d for d in info['peerDeps'] if d in internal_names]
    if internal_deps or internal_dev or internal_peer:
        print(f'{name}:')
        for d in internal_deps: print(f'  → {d} (dependency)')
        for d in internal_dev: print(f'  → {d} (devDependency)')
        for d in internal_peer: print(f'  → {d} (peerDependency)')
    else:
        print(f'{name}: (no internal deps — leaf package)')
" 2>/dev/null

For Cargo/Go workspaces, parse respective config files similarly.

Generate a Mermaid dependency diagram:

graph LR
  A[app] --> B[ui-lib]
  A --> C[api-client]
  B --> D[utils]
  C --> D

3. circular — Detect Circular Dependencies

python3 -c "
import json, glob, os

packages = {}
for pj in glob.glob('**/package.json', recursive=True):
    if 'node_modules' in pj: continue
    try:
        d = json.load(open(pj))
        name = d.get('name')
        if name:
            all_deps = set(d.get('dependencies', {}).keys()) | set(d.get('devDependencies', {}).keys())
            packages[name] = all_deps
    except: pass

internal = set(packages.keys())

# DFS cycle detection
def find_cycles(graph, internal):
    cycles = []
    visited = set()
    path = []
    path_set = set()

    def dfs(node):
        if node in path_set:
            cycle_start = path.index(node)
            cycles.append(path[cycle_start:] + [node])
            return
        if node in visited:
            return
        visited.add(node)
        path.append(node)
        path_set.add(node)
        for dep in graph.get(node, set()):
            if dep in internal:
                dfs(dep)
        path.pop()
        path_set.discard(node)

    for node in graph:
        if node in internal:
            dfs(node)
    return cycles

cycles = find_cycles(packages, internal)
if cycles:
    print(f'⚠️  Found {len(cycles)} circular dependency chain(s):')
    for c in cycles:
        print(f'  {\" → \".join(c)}')
else:
    print('✅ No circular dependencies found')
" 2>/dev/null

4. versions — Version Consistency Check

Find cases where different packages specify different versions of the same external dependency.

python3 -c "
import json, glob
from collections import defaultdict

dep_versions = defaultdict(dict)

for pj in glob.glob('**/package.json', recursive=True):
    if 'node_modules' in pj: continue
    try:
        d = json.load(open(pj))
        name = d.get('name', pj)
        for dep_type in ['dependencies', 'devDependencies']:
            for dep, ver in d.get(dep_type, {}).items():
                dep_versions[dep][name] = ver
    except: pass

# Find inconsistencies
mismatches = {}
for dep, consumers in dep_versions.items():
    versions = set(consumers.values())
    if len(versions) > 1:
        mismatches[dep] = consumers

if mismatches:
    print(f'⚠️  Found {len(mismatches)} dependencies with version mismatches:')
    for dep, consumers in sorted(mismatches.items()):
        print(f'  {dep}:')
        for pkg, ver in sorted(consumers.items()):
            print(f'    {pkg}: {ver}')
else:
    print('✅ All shared dependencies use consistent versions')
" 2>/dev/null

5. unused — Find Unused Packages

Packages defined in the workspace but not depended on by any other package (and not the root app).

python3 -c "
import json, glob

packages = {}
all_internal_deps = set()

for pj in glob.glob('**/package.json', recursive=True):
    if 'node_modules' in pj: continue
    try:
        d = json.load(open(pj))
        name = d.get('name')
        if name:
            packages[name] = d
            for dt in ['dependencies', 'devDependencies', 'peerDependencies']:
                all_internal_deps.update(d.get(dt, {}).keys())
    except: pass

internal_names = set(packages.keys())
unused = internal_names - all_internal_deps

# Filter: packages with 'start' or 'serve' scripts are likely apps, not libraries
true_unused = []
for name in unused:
    scripts = packages[name].get('scripts', {})
    is_app = any(k in scripts for k in ['start', 'serve', 'dev'])
    if is_app:
        print(f'  {name} (app entry point — not counted as unused)')
    else:
        true_unused.append(name)

if true_unused:
    print(f'⚠️  {len(true_unused)} potentially unused packages:')
    for name in sorted(true_unused):
        print(f'  {name}')
else:
    print('✅ No unused packages found')
" 2>/dev/null

6. build-order — Compute Topological Build Order

python3 -c "
import json, glob
from collections import defaultdict, deque

packages = {}
for pj in glob.glob('**/package.json', recursive=True):
    if 'node_modules' in pj: continue
    try:
        d = json.load(open(pj))
        name = d.get('name')
        if name:
            deps = set(d.get('dependencies', {}).keys()) | set(d.get('devDependencies', {}).keys())
            packages[name] = deps
    except: pass

internal = set(packages.keys())

# Topological sort (Kahn's algorithm)
in_degree = defaultdict(int)
graph = defaultdict(list)
for name in internal:
    if name not in in_degree: in_degree[name] = 0
    for dep in packages.get(name, set()):
        if dep in internal:
            graph[dep].append(name)
            in_degree[name] += 1

queue = deque([n for n in internal if in_degree[n] == 0])
order = []
while queue:
    node = queue.popleft()
    order.append(node)
    for neighbor in graph[node]:
        in_degree[neighbor] -= 1
        if in_degree[neighbor] == 0:
            queue.append(neighbor)

if len(order) == len(internal):
    print('Build order (leaf dependencies first):')
    for i, name in enumerate(order, 1):
        print(f'  {i}. {name}')
else:
    print('⚠️  Cannot determine full build order — circular dependencies exist')
    print(f'  Ordered: {len(order)}/{len(internal)} packages')
" 2>/dev/null

7. stats — Monorepo Statistics

# Package count and sizes
echo "=== Package Stats ==="
find . -maxdepth 3 -name "package.json" -not -path '*/node_modules/*' 2>/dev/null | wc -l
echo "packages found"

# Lines of code per package
for pj in $(find . -maxdepth 3 -name "package.json" -not -path '*/node_modules/*' 2>/dev/null); do
  DIR=$(dirname "$pj")
  NAME=$(python3 -c "import json; print(json.load(open('$pj')).get('name','$DIR'))" 2>/dev/null)
  LOC=$(find "$DIR" -type f \( -name "*.ts" -o -name "*.js" -o -name "*.tsx" -o -name "*.jsx" \) \
    -not -path '*/node_modules/*' -not -path '*/dist/*' 2>/dev/null | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}')
  echo "  $NAME: ${LOC:-0} lines"
done

# Git activity per package (last 30 days)
echo "=== Recent Activity (30 days) ==="
for pj in $(find . -maxdepth 3 -name "package.json" -not -path '*/node_modules/*' 2>/dev/null); do
  DIR=$(dirname "$pj")
  NAME=$(python3 -c "import json; print(json.load(open('$pj')).get('name','$DIR'))" 2>/dev/null)
  COMMITS=$(git log --since="30 days ago" --oneline -- "$DIR" 2>/dev/null | wc -l)
  if [ "$COMMITS" -gt 0 ]; then
    echo "  $NAME: $COMMITS commits"
  fi
done

Output Formats

  • text (default): Human-readable report with sections
  • json: Machine-readable {tool, packages: [{name, path, deps, devDeps}], graph: {edges}, cycles: [], mismatches: {}, unused: []}
  • markdown: Wiki-ready document with Mermaid diagrams
  • mermaid: Pure Mermaid dependency graph

CI Integration

Exit codes:

  • 0: No issues found
  • 1: Circular dependencies detected
  • 2: Version mismatches exceed threshold (default: 5)
# GitHub Actions
- name: Check monorepo health
  run: |
    # Agent runs: monorepo-analyzer circular
    # Agent runs: monorepo-analyzer versions
    # Exits 1 if circular deps or too many mismatches

Notes

  • Supports JS/TS (npm/yarn/pnpm/lerna/nx/turbo), Rust (cargo), and Go (go.work) monorepos
  • Does not install dependencies — works from config files only
  • For very large monorepos (500+ packages), the full scan may take a minute
  • Build order assumes no circular deps — run circular first
  • Version consistency check covers external deps only — internal workspace deps use workspace:* protocol