pub mod agent;
pub mod checkpoint;
pub mod completions;
pub mod config;
pub mod context;
pub mod decision;
pub mod doctor;
pub mod event;
pub mod graph;
pub mod handoff;
pub mod hooks;
pub mod init;
pub mod mcp;
pub mod preset;
pub mod progress;
pub mod project;
pub mod resume;
pub mod search;
pub mod session;
pub mod skill;
pub mod stats;
pub mod status;
pub mod sync;
pub mod task;
pub mod team;
pub mod worktree;
pub use agent::*;
pub use checkpoint::*;
pub use completions::*;
pub use config::*;
pub use context::*;
pub use decision::*;
pub use doctor::*;
pub use event::*;
pub use graph::*;
pub use handoff::*;
pub use hooks::*;
pub use init::*;
pub use mcp::*;
pub use preset::*;
pub use progress::*;
pub use project::*;
pub use resume::*;
pub use search::*;
pub use session::*;
pub use skill::*;
pub use stats::*;
pub use status::*;
pub use sync::*;
pub use task::*;
pub use team::*;
pub use worktree::*;
use carryctx::application::runtime::InvocationContext;
use carryctx::error::{CarryCtxError, ExitCode};
pub fn subcommand_label(prefix: &str, command: &impl std::fmt::Debug) -> String {
let variant = format!("{command:?}")
.split([' ', '(', '{'])
.next()
.unwrap_or("")
.to_ascii_lowercase();
format!("{prefix}.{variant}")
}
pub fn truncate_chars(s: &str, max_chars: usize) -> String {
if max_chars == 0 {
return String::new();
}
match s.char_indices().nth(max_chars) {
Some((idx, _)) => s[..idx].to_string(),
None => s.to_string(),
}
}
pub fn print_markdown_result<T, F>(
command: &str,
result: Result<T, CarryCtxError>,
render: F,
ctx: &InvocationContext,
) -> Result<ExitCode, ExitCode>
where
F: FnOnce(T) -> String,
{
match result {
Ok(value) => {
let md = render(value);
if !ctx.quiet {
print!("{md}");
}
Ok(ExitCode::Success)
}
Err(err) => crate::render_and_print_entity::<serde_json::Value>(
command,
Err(err),
matches!(
ctx.format,
carryctx::application::runtime::OutputFormat::Json
),
ctx.quiet,
false,
None,
None,
),
}
}
pub fn check_dry_run_envelope(
ctx: &InvocationContext,
command: &str,
description: &str,
) -> Option<Result<ExitCode, ExitCode>> {
if !ctx.dry_run {
return None;
}
let is_json = matches!(
ctx.format,
carryctx::application::runtime::OutputFormat::Json
);
eprintln!("[dry-run] Would {description}");
if is_json {
Some(crate::render_and_print_entity::<serde_json::Value>(
command,
Ok(serde_json::json!({"operation": {"applied": false}})),
true,
ctx.quiet,
false,
None,
None,
))
} else {
Some(Ok(ExitCode::Success))
}
}
pub fn render_dry_run_error(
command: &str,
err: CarryCtxError,
ctx: &InvocationContext,
) -> ExitCode {
crate::render_and_print_entity::<serde_json::Value>(
command,
Err(err),
true,
ctx.quiet,
false,
None,
None,
)
.err()
.unwrap_or(ExitCode::General)
}
#[allow(clippy::too_many_arguments)]
pub fn resolve_or_render<T>(
command: &str,
result: Result<T, CarryCtxError>,
ctx: &InvocationContext,
is_json: bool,
verbose: bool,
cli_fields: Option<&[String]>,
config_fields: Option<&std::collections::HashMap<String, Vec<String>>>,
) -> Result<T, ExitCode> {
result.map_err(|err| {
crate::render_and_print_entity::<serde_json::Value>(
command,
Err(err),
is_json,
ctx.quiet,
verbose,
cli_fields,
config_fields,
)
.err()
.unwrap_or(ExitCode::General)
})
}
#[cfg(test)]
mod shared_helper_tests {
use super::*;
#[test]
fn truncate_chars_is_char_boundary_safe() {
assert_eq!(truncate_chars("hello", 3), "hel");
assert_eq!(truncate_chars("hi", 8), "hi");
assert_eq!(truncate_chars("", 4), "");
assert_eq!(truncate_chars("日本語テスト", 3), "日本語");
assert_eq!(truncate_chars("🚀🚀🚀", 2), "🚀🚀");
}
#[test]
fn truncate_chars_never_panics_on_multibyte_input() {
let samples = ["中文标题很长很长", "mix中英text混排", "\u{1F600}\u{1F600}"];
for s in samples {
for n in 0..=12 {
let cut = truncate_chars(s, n);
assert!(cut.is_char_boundary(cut.len()));
assert!(cut.chars().count() <= n);
}
}
}
#[test]
fn truncate_chars_zero_yields_empty() {
assert_eq!(truncate_chars("abc", 0), "");
}
}