use std::process::ExitCode;
use serde::Serialize;
use super::behavior::{self, CommandBehavior};
use crate::cli_command;
use crate::extensions;
use crate::memory::provider as memory_provider;
use crate::output::CommandReport;
use crate::runtime_api;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum CommandTier {
Core,
Advanced,
Platform,
}
#[derive(Serialize)]
pub struct DescribeReport {
command: &'static str,
ok: bool,
version: String,
about: Option<String>,
runtime_api: runtime_api::RuntimeApiView,
active_extensions: Vec<ActiveExtensionView>,
memory_provider: memory_provider::MemoryProviderView,
global_args: Vec<ArgDescriptor>,
pub(crate) commands: Vec<CommandDescriptor>,
}
#[derive(Serialize)]
struct ActiveExtensionView {
name: &'static str,
command_groups: Vec<&'static str>,
}
#[derive(Serialize)]
pub(crate) struct CommandDescriptor {
pub(crate) name: String,
pub(crate) about: Option<String>,
pub(crate) tier: CommandTier,
pub(crate) args: Vec<ArgDescriptor>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) behavior: Option<CommandBehavior>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) subcommands: Vec<CommandDescriptor>,
}
#[derive(Serialize)]
pub(crate) struct ArgDescriptor {
pub(crate) name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) short: Option<char>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) long: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) aliases: Option<Vec<String>>,
pub(crate) required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) default: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) possible_values: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) help: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) value_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) value_delimiter: Option<char>,
}
fn is_visible_arg(arg: &clap::Arg) -> bool {
!arg.is_hide_set() && arg.get_id() != "help" && arg.get_id() != "version"
}
fn push_unique(target: &mut Vec<String>, value: &str) {
if !target.iter().any(|existing| existing == value) {
target.push(value.to_owned());
}
}
fn describe_arg(arg: &clap::Arg) -> ArgDescriptor {
let default = {
let vals: Vec<&str> = arg
.get_default_values()
.iter()
.filter_map(|v| v.to_str())
.collect();
if vals.is_empty() {
None
} else {
Some(vals.join(","))
}
};
let possible_values = {
let pv = arg.get_possible_values();
if pv.is_empty() {
None
} else {
let mut values = Vec::new();
for value in pv {
for name in value.get_name_and_aliases() {
push_unique(&mut values, name);
}
}
Some(values)
}
};
let aliases = {
let values = arg
.get_all_aliases()
.unwrap_or_default()
.into_iter()
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
if values.is_empty() {
None
} else {
Some(values)
}
};
ArgDescriptor {
name: arg.get_id().to_string(),
short: arg.get_short(),
long: arg.get_long().map(|s| s.to_owned()),
aliases,
required: arg.is_required_set(),
default,
possible_values,
help: arg.get_help().map(|s| s.to_string()),
value_name: arg.get_value_names().and_then(|names| {
let v: Vec<&str> = names.iter().map(|n| n.as_str()).collect();
if v.is_empty() {
None
} else {
Some(v.join(","))
}
}),
value_delimiter: arg.get_value_delimiter(),
}
}
fn describe_command(cmd: &clap::Command, parents: &[String]) -> CommandDescriptor {
let mut path = parents.to_vec();
path.push(cmd.get_name().to_owned());
let path_refs: Vec<&str> = path.iter().map(|segment| segment.as_str()).collect();
let args: Vec<ArgDescriptor> = cmd
.get_arguments()
.filter(|a| is_visible_arg(a))
.map(describe_arg)
.collect();
let subcommands: Vec<CommandDescriptor> = cmd
.get_subcommands()
.filter(|s| s.get_name() != "help" && !s.is_hide_set())
.map(|sub| describe_command(sub, &path))
.collect();
CommandDescriptor {
name: cmd.get_name().to_owned(),
about: cmd.get_about().map(|s| s.to_string()),
tier: tier_for_command_path(&path_refs),
args,
behavior: behavior::for_command_path(&path_refs),
subcommands,
}
}
fn tier_for_command_path(path: &[&str]) -> CommandTier {
match path {
["attach" | "init" | "start" | "status" | "checkpoint" | "handover" | "doctor" | "sync"] => {
CommandTier::Core
}
["session-state", "start" | "clear"] => CommandTier::Core,
["context-check"]
| ["policy-check"]
| ["runtime-state", ..]
| ["escalation-state", ..]
| ["session-state", ..]
| ["pod", ..] => CommandTier::Platform,
_ => CommandTier::Advanced,
}
}
pub fn run() -> DescribeReport {
let cmd = cli_command();
let version = cmd.get_version().unwrap_or("unknown").to_owned();
let about = cmd.get_about().map(|s| s.to_string());
let global_args: Vec<ArgDescriptor> = cmd
.get_arguments()
.filter(|a| is_visible_arg(a))
.map(describe_arg)
.collect();
let commands: Vec<CommandDescriptor> = cmd
.get_subcommands()
.filter(|s| s.get_name() != "help" && !s.is_hide_set())
.map(|sub| describe_command(sub, &[]))
.collect();
let active_extensions = extensions::registered()
.into_iter()
.map(|extension| ActiveExtensionView {
name: extension.name(),
command_groups: extension.command_groups().to_vec(),
})
.collect();
DescribeReport {
command: "describe",
ok: true,
version,
about,
runtime_api: runtime_api::describe_runtime_api(),
active_extensions,
memory_provider: memory_provider::inspect_current_dir_for_describe(),
global_args,
commands,
}
}
impl CommandReport for DescribeReport {
fn exit_code(&self) -> ExitCode {
ExitCode::SUCCESS
}
fn render_text(&self) {
println!(
"ccd {} — {}",
self.version,
self.about.as_deref().unwrap_or("")
);
println!();
render_tier_section("Core Commands", CommandTier::Core, &self.commands);
render_tier_section("Advanced Commands", CommandTier::Advanced, &self.commands);
render_tier_section("Platform Commands", CommandTier::Platform, &self.commands);
println!();
println!("Run `ccd describe --output json` for full structured metadata.");
}
}
fn render_tier_section(title: &str, tier: CommandTier, commands: &[CommandDescriptor]) {
if !commands.iter().any(|cmd| subtree_contains_tier(cmd, tier)) {
return;
}
println!("{title}:");
for cmd in commands {
render_command_text_for_tier(cmd, tier, 1);
}
println!();
}
fn subtree_contains_tier(cmd: &CommandDescriptor, tier: CommandTier) -> bool {
cmd.tier == tier
|| cmd
.subcommands
.iter()
.any(|sub| subtree_contains_tier(sub, tier))
}
fn render_command_text_for_tier(cmd: &CommandDescriptor, tier: CommandTier, depth: usize) {
let mut matching_children = Vec::new();
for sub in &cmd.subcommands {
if subtree_contains_tier(sub, tier) {
matching_children.push(sub);
}
}
let should_render_self = cmd.tier == tier;
if !should_render_self && matching_children.is_empty() {
return;
}
let indent = " ".repeat(depth);
let about = cmd.about.as_deref().unwrap_or("");
if about.is_empty() {
println!("{indent}{}", cmd.name);
} else {
println!("{indent}{:<24}{about}", cmd.name);
}
for sub in matching_children {
render_command_text_for_tier(sub, tier, depth + 1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn describe_reflects_registered_extensions() {
let report = run();
let names = report
.commands
.iter()
.map(|command| command.name.as_str())
.collect::<Vec<_>>();
assert!(!names.contains(&"backlog"));
}
}