use crate::render_and_print;
use crate::try_open_runtime;
use carryctx::application::runtime::InvocationContext;
use carryctx::domain::graph::{GraphEdge, GraphNode};
use carryctx::error::ExitCode;
use carryctx::output::{OutputSink, render_json};
use chrono::Utc;
use clap::{Args, Parser, Subcommand};
use serde_json::json;
#[derive(Parser, Debug)]
pub struct GraphArgs {
#[command(subcommand)]
pub command: GraphSubcommands,
}
#[derive(Subcommand, Debug)]
pub enum GraphSubcommands {
Edges(GraphEdgesArgs),
AddNode(AddNodeArgs),
Link(LinkArgs),
ExtractDeps(ExtractDepsArgs),
Scan(ScanArgs),
Export(ExportArgs),
}
#[derive(Args, Debug)]
pub struct GraphEdgesArgs {
#[arg(help = "The ULID of the node")]
pub id: String,
}
#[derive(Args, Debug)]
pub struct AddNodeArgs {
#[arg(long, help = "Type of node (e.g., file, module, decision)")]
pub node_type: String,
#[arg(long, help = "Name of the node")]
pub name: String,
#[arg(long, help = "Description of the node")]
pub description: Option<String>,
}
#[derive(Args, Debug)]
pub struct LinkArgs {
#[arg(help = "Source node ULID")]
pub source: String,
#[arg(help = "Target node ULID")]
pub target: String,
pub relation: String,
}
#[derive(Args, Debug)]
pub struct ExtractDepsArgs {
#[arg(help = "The file path to extract dependencies from")]
pub file: String,
}
#[derive(Args, Debug)]
pub struct ScanArgs {
#[arg(long, default_value = ".")]
pub dir: String,
#[arg(long, default_value = "rs,ts,js,tsx,jsx")]
pub ext: String,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Args, Debug)]
pub struct ExportArgs {
#[arg(
short = 't',
long = "type",
alias = "format",
default_value = "mermaid"
)]
pub export_format: String,
#[arg(short, long)]
pub output: Option<String>,
#[arg(long)]
pub node_type: Option<String>,
#[arg(long)]
pub focus: Option<String>,
#[arg(long, default_value_t = 1)]
pub depth: usize,
#[arg(long)]
pub compact: bool,
#[arg(long)]
pub ascii: bool,
}
pub fn handle_graph(
args: &GraphArgs,
ctx: &InvocationContext,
is_json: bool,
) -> Result<ExitCode, ExitCode> {
let runtime = try_open_runtime(ctx)?;
let conn = runtime.database.connection();
let repo = carryctx::repository::GraphRepository::new(conn);
match &args.command {
GraphSubcommands::Edges(cmd) => {
let result = repo.get_edges_for_node(&cmd.id);
let (out, sink, code) = render_json("graph edges", result.as_ref(), is_json);
match sink {
OutputSink::Stdout => println!("{}", out),
OutputSink::Stderr => eprintln!("{}", out),
}
if code == ExitCode::Success {
Ok(code)
} else {
Err(code)
}
}
GraphSubcommands::AddNode(cmd) => {
let id = ulid::Ulid::generate().to_string();
let now = Utc::now().to_rfc3339();
let node = GraphNode::new(
&id,
&cmd.node_type,
&cmd.name,
cmd.description.clone(),
json!({}),
now,
);
let result = repo.insert_node(&node).map(|_| node);
let (out, sink, code) = render_json("graph add-node", result.as_ref(), is_json);
match sink {
OutputSink::Stdout => println!("{}", out),
OutputSink::Stderr => eprintln!("{}", out),
}
if code == ExitCode::Success {
Ok(code)
} else {
Err(code)
}
}
GraphSubcommands::Link(cmd) => {
let now = Utc::now().to_rfc3339();
let edge = GraphEdge::new(
&cmd.source,
&cmd.target,
&cmd.relation,
now,
ctx.agent.clone(),
json!({}),
);
let result = repo.insert_edge(&edge).map(|_| edge);
let (out, sink, code) = render_json("graph link", result.as_ref(), is_json);
match sink {
OutputSink::Stdout => println!("{}", out),
OutputSink::Stderr => eprintln!("{}", out),
}
if code == ExitCode::Success {
Ok(code)
} else {
Err(code)
}
}
GraphSubcommands::ExtractDeps(cmd) => {
let result =
carryctx::application::extract_deps::extract_deps_for_file(&cmd.file, &repo, ctx);
let (out, sink, code) = render_json("graph extract-deps", result.as_ref(), is_json);
match sink {
OutputSink::Stdout => println!("{}", out),
OutputSink::Stderr => eprintln!("{}", out),
}
if code == ExitCode::Success {
Ok(code)
} else {
Err(code)
}
}
GraphSubcommands::Scan(cmd) => {
use carryctx::application::scan_graph::{DEFAULT_EXTENSIONS, scan_project};
use std::path::Path;
let ext_owned: Vec<String> = cmd.ext.split(',').map(|s| s.trim().to_string()).collect();
let extensions: Vec<&str> = ext_owned.iter().map(|s| s.as_str()).collect();
let extensions: &[&str] = if extensions.is_empty() {
DEFAULT_EXTENSIONS
} else {
&extensions
};
let dir = Path::new(&cmd.dir);
let scan_result = scan_project(dir, extensions, cmd.dry_run, &repo, ctx);
let result = scan_result.map(|r| {
let errors: Vec<serde_json::Value> = r
.errors
.iter()
.map(|e| json!({ "file": e.file, "error": e.message }))
.collect();
json!({
"dryRun": cmd.dry_run,
"extensions": extensions,
"scanned": r.scanned,
"skipped": r.skipped,
"nodesCreated": r.nodes_created,
"edgesCreated": r.edges_created,
"errorCount": errors.len(),
"errors": errors,
})
});
let (out, sink, code) = render_json("graph scan", result.as_ref(), is_json);
match sink {
OutputSink::Stdout => println!("{}", out),
OutputSink::Stderr => eprintln!("{}", out),
}
if code == ExitCode::Success {
Ok(code)
} else {
Err(code)
}
}
GraphSubcommands::Export(cmd) => {
use carryctx::application::export_graph::{
GraphExportFormat, export_graph, render_image_to_file,
};
use std::str::FromStr;
let fmt_str = if cmd.ascii {
"ascii"
} else {
cmd.export_format.as_str()
};
let result: Result<serde_json::Value, carryctx::error::CarryCtxError> = (|| {
let parsed_format = GraphExportFormat::from_str(fmt_str)?;
let content = export_graph(
&repo,
parsed_format,
cmd.node_type.as_deref(),
cmd.focus.as_deref(),
cmd.depth,
cmd.compact,
)?;
if let Some(out_path) = &cmd.output {
render_image_to_file(&content, parsed_format, out_path)?;
Ok(json!({
"status": "success",
"format": fmt_str,
"outputPath": out_path,
}))
} else {
Ok(json!({
"status": "success",
"format": fmt_str,
"content": content,
}))
}
})(
);
match result {
Ok(data) => {
if is_json {
let (out, _, code) = render_json("graph export", Ok(data), true);
println!("{}", out);
Ok(code)
} else if let Some(content) = data["content"].as_str() {
print!("{}", content);
Ok(ExitCode::Success)
} else if let Some(path) = data["outputPath"].as_str() {
println!("Successfully exported graph to {}", path);
Ok(ExitCode::Success)
} else {
Ok(ExitCode::Success)
}
}
Err(err) => render_and_print("graph.export", Err::<(), _>(err), is_json, ctx.quiet),
}
}
}
}