use clap::Parser;
use serde::Serialize;
use serde_json::Value;
pub mod commands;
pub mod types;
pub use commands::*;
pub use types::*;
#[derive(Parser, Debug)]
#[command(name = "splice")]
#[command(
author,
version,
about,
long_about = "
Splice: Span-safe refactoring kernel for Rust.
Query Commands (Magellan-delegated):
status, find, refs, files, query Query code graph database
Graph Algorithm Commands:
reachable, dead-code, cycles Analyze code structure
condense, slice Impact analysis and slicing
Edit Commands:
delete, patch, plan, apply-files Modify code with span safety
Export Commands:
log, undo, export Export and restore operations
Validation Commands:
explain, search, get Validate and explain code
Use 'splice help <command>' for more information on a specific command.
Options:
-v, --verbose Enable verbose logging
-o, --output <FORMAT> Output format (human, json, pretty)
--json Output JSON (deprecated: use --output json)
--strict Enable strict pre-verification
-h, --help Print help
-V, --version Print version
"
)]
#[command(subcommand_required = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(short, long, global = true)]
pub verbose: bool,
#[arg(short, long, global = true, value_enum, default_value_t = OutputFormat::Human)]
pub output: OutputFormat,
#[arg(long, global = true, hide = true)]
json: bool,
#[arg(long, global = true)]
pub strict: bool,
#[arg(long, global = true, hide = true)]
pub skip_pre_verify: bool,
}
pub fn parse_args() -> Cli {
Cli::parse()
}
impl Cli {
pub fn json_output(&self) -> bool {
if self.json {
return true;
}
self.output.is_json()
}
pub fn output_format(&self) -> OutputFormat {
if self.json {
return OutputFormat::Json;
}
self.output
}
}
#[derive(Serialize)]
pub struct CliSuccessPayload {
pub status: &'static str,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
#[serde(skip)]
pub already_emitted: bool,
#[serde(skip)]
pub has_pending_changes: bool,
}
impl CliSuccessPayload {
pub fn message_only(message: String) -> Self {
Self {
status: "ok",
message,
data: None,
already_emitted: false,
has_pending_changes: false,
}
}
pub fn with_data(message: String, data: Value) -> Self {
Self {
status: "ok",
message,
data: Some(data),
already_emitted: false,
has_pending_changes: false,
}
}
pub fn already_emitted(mut self) -> Self {
self.already_emitted = true;
self
}
pub fn with_pending_changes(mut self) -> Self {
self.has_pending_changes = true;
self
}
}
#[derive(Serialize)]
pub struct CliErrorPayload {
pub status: &'static str,
pub error: ErrorDetails,
}
#[derive(Serialize)]
pub struct ErrorDetails {
pub kind: &'static str,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub diagnostics: Option<Vec<DiagnosticPayload>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_code: Option<crate::ErrorCode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub explain_command: Option<String>,
}
impl CliErrorPayload {
pub fn from_error(error: &crate::SpliceError) -> Self {
let symbol = error.symbol().map(|s| s.to_string());
let file = error
.file_path()
.and_then(|p| p.to_str().map(|s| s.to_string()));
let hint = error.hint().map(|h| h.to_string());
let diagnostics = {
let diagnostics = error.diagnostics();
if diagnostics.is_empty() {
None
} else {
Some(
diagnostics
.into_iter()
.map(DiagnosticPayload::from)
.collect(),
)
}
};
let error_code =
crate::error_codes::SpliceErrorCode::from_splice_error(error).map(|splice_code| {
let (file, line, column) = error.location();
crate::ErrorCode::from_splice_code(splice_code, file, line, column)
});
let explain_command = error_code
.as_ref()
.map(|ec| format!("splice explain --code {}", ec.code));
CliErrorPayload {
status: "error",
error: ErrorDetails {
kind: error.kind(),
message: error.to_string(),
symbol,
file,
hint,
diagnostics,
error_code,
explain_command,
},
}
}
}
#[derive(Serialize)]
pub struct DiagnosticPayload {
pub tool: String,
pub level: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remediation: Option<String>,
}
impl From<crate::error::Diagnostic> for DiagnosticPayload {
fn from(diag: crate::error::Diagnostic) -> Self {
DiagnosticPayload {
tool: diag.tool,
level: diag.level.as_str().to_string(),
message: diag.message,
file: diag
.file
.as_ref()
.and_then(|p| p.to_str().map(|s| s.to_string())),
line: diag.line,
column: diag.column,
code: diag.code,
note: diag.note,
tool_path: diag
.tool_path
.as_ref()
.and_then(|p| p.to_str().map(|s| s.to_string())),
tool_version: diag.tool_version,
remediation: diag.remediation,
}
}
}
#[cfg(test)]
mod tests;
pub use crate::output::{
CallExport, ExportData, ExportResponse, FileExport, FilesResponse, FindResponse,
MagellanCallReference, MagellanFileMetadata, MagellanSpan, MagellanSymbol, ReferenceExport,
RefsResponse, StatusResponse, SymbolExport, EXPORT_SCHEMA_VERSION,
};