use anyhow::{bail, Result};
use clap::{Parser, Subcommand};
use std::path::{Path, PathBuf};
mod commands;
mod config;
mod core;
mod languages;
mod output;
#[derive(Parser, Debug)]
#[command(
name = "scope",
about = "Code intelligence CLI for LLM coding agents",
long_about = "Scope builds a local code intelligence index and lets you query \
it efficiently. Use it before editing any non-trivial code to \
understand structure, dependencies, and blast radius.",
version,
propagate_version = true
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(long, global = true)]
pub verbose: bool,
#[arg(long, global = true)]
pub workspace: bool,
#[arg(long, global = true)]
pub project: Option<String>,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Init(commands::init::InitArgs),
Index(commands::index::IndexArgs),
Sketch(commands::sketch::SketchArgs),
Refs(commands::refs::RefsArgs),
Callers(commands::refs::CallersArgs),
Deps(commands::deps::DepsArgs),
Rdeps(commands::rdeps::RdepsArgs),
Impact(commands::impact::ImpactArgs),
Diff(commands::diff::DiffArgs),
Flow(commands::flow::FlowArgs),
Find(commands::find::FindArgs),
Similar(commands::similar::SimilarArgs),
Summary(commands::summary::SummaryArgs),
Source(commands::source::SourceArgs),
Trace(commands::trace::TraceArgs),
Entrypoints(commands::entrypoints::EntrypointsArgs),
Map(commands::map::MapArgs),
Status(commands::status::StatusArgs),
Setup(commands::setup::SetupArgs),
Workspace(commands::workspace::WorkspaceArgs),
}
pub enum Context {
SingleProject {
root: PathBuf,
},
Workspace {
manifest_path: PathBuf,
workspace_root: PathBuf,
config: config::workspace::WorkspaceConfig,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
let level = if cli.verbose {
tracing::Level::DEBUG
} else {
tracing::Level::WARN
};
tracing_subscriber::fmt()
.with_max_level(level)
.with_writer(std::io::stderr)
.init();
let ctx = resolve_context(cli.workspace, cli.project.as_deref())?;
match &cli.command {
Commands::Status(args) => commands::status::run(args, &ctx),
Commands::Map(args) => commands::map::run(args, &ctx),
Commands::Refs(args) => commands::refs::run(args, &ctx),
Commands::Find(args) => commands::find::run(args, &ctx),
Commands::Entrypoints(args) => commands::entrypoints::run(args, &ctx),
Commands::Init(args) => {
let root = project_root_from_context(&ctx)?;
commands::init::run(args, root)
}
Commands::Index(args) => {
let root = project_root_from_context(&ctx)?;
commands::index::run(args, root)
}
Commands::Sketch(args) => {
let root = project_root_from_context(&ctx)?;
commands::sketch::run(args, root)
}
Commands::Callers(args) => {
let root = project_root_from_context(&ctx)?;
commands::refs::run_callers(args, root)
}
Commands::Deps(args) => {
let root = project_root_from_context(&ctx)?;
commands::deps::run(args, root)
}
Commands::Rdeps(args) => {
let root = project_root_from_context(&ctx)?;
commands::rdeps::run(args, root)
}
Commands::Impact(args) => {
let root = project_root_from_context(&ctx)?;
commands::impact::run(args, root)
}
Commands::Similar(args) => {
let root = project_root_from_context(&ctx)?;
commands::similar::run(args, root)
}
Commands::Summary(args) => {
let root = project_root_from_context(&ctx)?;
commands::summary::run(args, root)
}
Commands::Source(args) => {
let root = project_root_from_context(&ctx)?;
commands::source::run(args, root)
}
Commands::Trace(args) => {
let root = project_root_from_context(&ctx)?;
commands::trace::run(args, root)
}
Commands::Diff(args) => {
let root = project_root_from_context(&ctx)?;
commands::diff::run(args, root)
}
Commands::Flow(args) => {
let root = project_root_from_context(&ctx)?;
commands::flow::run(args, root)
}
Commands::Setup(args) => {
let root = project_root_from_context(&ctx)?;
commands::setup::run(args, root)
}
Commands::Workspace(args) => {
let root = cwd()?;
commands::workspace::run(args, &root)
}
}
}
fn resolve_context(workspace_flag: bool, project_flag: Option<&str>) -> Result<Context> {
let cwd = cwd()?;
if let Some(project_name) = project_flag {
let manifest_path = commands::workspace::find_workspace_manifest(&cwd)?;
let workspace_root = manifest_path.parent().unwrap_or(&cwd).to_path_buf();
let config = config::workspace::WorkspaceConfig::load(&manifest_path)?;
config.validate(&workspace_root)?;
let member = config
.workspace
.members
.iter()
.find(|m| config::workspace::WorkspaceConfig::resolve_member_name(m) == project_name)
.ok_or_else(|| {
let available: Vec<String> = config
.workspace
.members
.iter()
.map(config::workspace::WorkspaceConfig::resolve_member_name)
.collect();
anyhow::anyhow!(
"Project '{}' not found in workspace. Available: {}",
project_name,
available.join(", ")
)
})?;
let member_root = workspace_root.join(&member.path);
return Ok(Context::SingleProject { root: member_root });
}
if workspace_flag {
let manifest_path = commands::workspace::find_workspace_manifest(&cwd)?;
let workspace_root = manifest_path.parent().unwrap_or(&cwd).to_path_buf();
let config = config::workspace::WorkspaceConfig::load(&manifest_path)?;
config.validate(&workspace_root)?;
return Ok(Context::Workspace {
manifest_path,
workspace_root,
config,
});
}
Ok(Context::SingleProject { root: cwd })
}
fn cwd() -> Result<PathBuf> {
std::env::current_dir().map_err(|e| anyhow::anyhow!("Failed to get current directory: {e}"))
}
fn project_root_from_context(ctx: &Context) -> Result<&Path> {
match ctx {
Context::SingleProject { root } => Ok(root),
Context::Workspace { .. } => {
bail!(
"This command operates on a single project. \
Use --project <name> to target a workspace member."
)
}
}
}