use anyhow::{bail, Result};
use clap::Args;
use serde::Serialize;
use std::path::Path;
use crate::config::workspace::WorkspaceConfig;
use crate::core::graph::Graph;
use crate::core::workspace_graph::WorkspaceGraph;
use crate::output::formatter;
use crate::output::json::JsonOutput;
use crate::Context;
#[derive(Args, Debug)]
pub struct EntrypointsArgs {
#[arg(long, short = 'j')]
pub json: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct EntrypointInfo {
pub name: String,
pub file_path: String,
pub method_count: usize,
pub outgoing_call_count: usize,
pub kind: String,
}
pub fn classify_group(file_path: &str) -> &'static str {
let lower = file_path.to_lowercase();
if lower.contains("controller") {
"API Controllers"
} else if lower.contains("worker") || lower.contains("job") {
"Background Workers"
} else if lower.contains("handler") || lower.contains("listener") {
"Event Handlers"
} else {
"Other"
}
}
pub(crate) fn collapse_and_group(
raw_entrypoints: &[(crate::core::graph::Symbol, usize)],
graph: &Graph,
) -> (Vec<(String, Vec<EntrypointInfo>)>, usize, usize) {
let mut class_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut class_method_counts: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
for (sym, _) in raw_entrypoints {
if sym.kind == "class" {
class_ids.insert(sym.id.clone());
let methods = graph.get_methods(&sym.id).unwrap_or_default();
class_method_counts.insert(sym.id.clone(), methods.len());
}
}
let mut infos: Vec<EntrypointInfo> = Vec::new();
for (sym, outgoing) in raw_entrypoints {
if let Some(ref parent) = sym.parent_id {
if class_ids.contains(parent) {
continue;
}
}
let method_count = class_method_counts.get(&sym.id).copied().unwrap_or(0);
infos.push(EntrypointInfo {
name: sym.name.clone(),
file_path: sym.file_path.clone(),
method_count,
outgoing_call_count: *outgoing,
kind: sym.kind.clone(),
});
}
let unique_files: std::collections::HashSet<&str> =
infos.iter().map(|e| e.file_path.as_str()).collect();
let file_count = unique_files.len();
let total = infos.len();
let group_order = [
"API Controllers",
"Background Workers",
"Event Handlers",
"Other",
];
let mut groups: Vec<(String, Vec<EntrypointInfo>)> = Vec::new();
for &group_name in &group_order {
let members: Vec<EntrypointInfo> = infos
.iter()
.filter(|e| classify_group(&e.file_path) == group_name)
.cloned()
.collect();
if !members.is_empty() {
groups.push((group_name.to_string(), members));
}
}
(groups, total, file_count)
}
pub fn run(args: &EntrypointsArgs, ctx: &Context) -> Result<()> {
match ctx {
Context::SingleProject { root } => run_single(args, root),
Context::Workspace {
workspace_root,
config,
..
} => run_workspace(args, workspace_root, config),
}
}
fn run_single(args: &EntrypointsArgs, project_root: &Path) -> Result<()> {
let scope_dir = project_root.join(".scope");
if !scope_dir.exists() {
bail!("No .scope/ directory found. Run 'scope init' first.");
}
let db_path = scope_dir.join("graph.db");
if !db_path.exists() {
bail!("No index found. Run 'scope index' to build one first.");
}
let graph = Graph::open(&db_path)?;
crate::commands::warn_if_stale(&graph, project_root);
let raw_entrypoints = graph.get_entrypoints()?;
let (groups, total, file_count) = collapse_and_group(&raw_entrypoints, &graph);
if args.json {
let output = JsonOutput {
command: "entrypoints",
symbol: None,
data: &groups,
truncated: false,
total,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_entrypoints(&groups, total, file_count);
}
Ok(())
}
fn run_workspace(
args: &EntrypointsArgs,
workspace_root: &Path,
config: &WorkspaceConfig,
) -> Result<()> {
let members: Vec<(String, std::path::PathBuf)> = config
.workspace
.members
.iter()
.map(|entry| {
let name = WorkspaceConfig::resolve_member_name(entry);
let path = workspace_root.join(&entry.path);
(name, path)
})
.collect();
let wg = WorkspaceGraph::open(members)?;
let mut all_groups: Vec<(String, Vec<EntrypointInfo>)> = Vec::new();
let mut total = 0usize;
let mut file_count = 0usize;
for member in wg.members() {
let raw = member.graph.get_entrypoints().unwrap_or_default();
let (member_groups, member_total, member_files) = collapse_and_group(&raw, &member.graph);
total += member_total;
file_count += member_files;
for (group_name, mut entries) in member_groups {
for info in &mut entries {
info.name = format!("{}::{}", member.name, info.name);
}
let label = format!("{} \u{2014} {group_name}", member.name);
all_groups.push((label, entries));
}
}
if args.json {
let output = JsonOutput {
command: "entrypoints",
symbol: None,
data: &all_groups,
truncated: false,
total,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_entrypoints(&all_groups, total, file_count);
}
Ok(())
}