plugins/claude/code/scripts/deps.py
Snider 8dacc91593
feat(/core:deps): show module dependencies (#82)
This commit introduces a new command, `/core:deps`, to visualize dependencies between modules in the monorepo.

The command parses a `repos.yaml` file to build a dependency graph and supports the following functionalities:
- Displaying a full dependency tree for all modules.
- Displaying a dependency tree for a single module.
- Displaying reverse dependencies for a single module using the `--reverse` flag.
- Detecting and reporting circular dependencies.

The implementation consists of a Python script that handles the core logic and a command definition file that connects the command to the script. A comprehensive test suite is included to ensure the correctness of the implementation.

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-02 07:19:58 +00:00

151 lines
4.6 KiB
Python

import os
import sys
import yaml
def find_repos_yaml():
"""Traverse up from the current directory to find repos.yaml."""
current_dir = os.getcwd()
while current_dir != '/':
repos_yaml_path = os.path.join(current_dir, 'repos.yaml')
if os.path.exists(repos_yaml_path):
return repos_yaml_path
current_dir = os.path.dirname(current_dir)
return None
def parse_dependencies(repos_yaml_path):
"""Parses the repos.yaml file and returns a dependency graph."""
with open(repos_yaml_path, 'r') as f:
data = yaml.safe_load(f)
graph = {}
repos = data.get('repos', {})
for repo_name, details in repos.items():
graph[repo_name] = details.get('depends', []) or []
return graph
def find_circular_dependencies(graph):
"""Finds circular dependencies in the graph using DFS."""
visiting = set()
visited = set()
cycles = []
def dfs(node, path):
visiting.add(node)
path.append(node)
for neighbor in graph.get(node, []):
if neighbor in visiting:
cycle_start_index = path.index(neighbor)
cycles.append(path[cycle_start_index:] + [neighbor])
elif neighbor not in visited:
dfs(neighbor, path)
path.pop()
visiting.remove(node)
visited.add(node)
for node in graph:
if node not in visited:
dfs(node, [])
return cycles
def print_dependency_tree(graph, module, prefix=""):
"""Prints the dependency tree for a given module."""
if module not in graph:
print(f"Module '{module}' not found.")
return
print(f"{prefix}{module}")
dependencies = graph.get(module, [])
for i, dep in enumerate(dependencies):
is_last = i == len(dependencies) - 1
new_prefix = prefix.replace("├──", "").replace("└──", " ")
connector = "└── " if is_last else "├── "
print_dependency_tree(graph, dep, new_prefix + connector)
def print_reverse_dependencies(graph, module):
"""Prints the modules that depend on a given module."""
if module not in graph:
print(f"Module '{module}' not found.")
return
reverse_deps = []
for repo, deps in graph.items():
if module in deps:
reverse_deps.append(repo)
if not reverse_deps:
print(f"(no modules depend on {module})")
else:
for i, dep in enumerate(sorted(reverse_deps)):
is_last = i == len(reverse_deps) - 1
print(f"{'└── ' if is_last else '├── '}{dep}")
def main():
"""Main function to handle command-line arguments and execute logic."""
repos_yaml_path = find_repos_yaml()
if not repos_yaml_path:
print("Error: Could not find repos.yaml in the current directory or any parent directory.")
sys.exit(1)
try:
graph = parse_dependencies(repos_yaml_path)
except Exception as e:
print(f"Error parsing repos.yaml: {e}")
sys.exit(1)
cycles = find_circular_dependencies(graph)
if cycles:
print("Error: Circular dependencies detected!")
for cycle in cycles:
print(" -> ".join(cycle))
sys.exit(1)
args = sys.argv[1:]
if not args:
print("Dependency tree for all modules:")
for module in sorted(graph.keys()):
print(f"\n{module} dependencies:")
dependencies = graph.get(module, [])
if not dependencies:
print("└── (no dependencies)")
else:
for i, dep in enumerate(dependencies):
is_last = i == len(dependencies) - 1
print_dependency_tree(graph, dep, "└── " if is_last else "├── ")
return
reverse = "--reverse" in args
if reverse:
args.remove("--reverse")
if not args:
print("Usage: /core:deps [--reverse] [module_name]")
sys.exit(1)
module_name = args[0]
if module_name not in graph:
print(f"Error: Module '{module_name}' not found in repos.yaml.")
sys.exit(1)
if reverse:
print(f"Modules that depend on {module_name}:")
print_reverse_dependencies(graph, module_name)
else:
print(f"{module_name} dependencies:")
dependencies = graph.get(module_name, [])
if not dependencies:
print("└── (no dependencies)")
else:
for i, dep in enumerate(dependencies):
is_last = i == len(dependencies) - 1
connector = "└── " if is_last else "├── "
print_dependency_tree(graph, dep, connector)
if __name__ == "__main__":
main()