use crate::core::contracts::{discovery::*, scalar::No};
use clap::{ArgAction, Command, CommandFactory};
use serde_json::{Map, Value, json};
use crate::cli::args::Cli;
use crate::core::error::{AppError, Result};
fn root() -> Command {
let mut command = Cli::command();
command.build();
command
}
pub fn list() -> Vec<CommandSummary> {
fn visit(command: &Command, prefix: &str, entries: &mut Vec<CommandSummary>) {
for child in command.get_subcommands().filter(|c| c.get_name() != "help") {
let name = if prefix.is_empty() {
child.get_name().to_owned()
} else {
format!("{prefix}.{}", child.get_name())
};
if child.has_subcommands() {
visit(child, &name, entries);
} else {
entries.push(CommandSummary {
name,
summary: child
.get_about()
.map(ToString::to_string)
.unwrap_or_default(),
});
}
}
}
let mut entries = Vec::new();
visit(&root(), "", &mut entries);
entries
}
pub fn get(path: &str) -> Result<Value> {
serde_json::to_value(describe(path)?).map_err(|_| AppError::protocol())
}
pub fn describe(path: &str) -> Result<Description> {
let command = root();
let mut selected = &command;
for part in path.split('.') {
selected = selected
.find_subcommand(part)
.filter(|c| c.get_name() != "help")
.ok_or_else(|| {
AppError::new("COMMAND_NOT_FOUND", "Unknown command; use schema list.", 2)
})?;
}
if selected.has_subcommands() || path.is_empty() {
return Err(AppError::new(
"COMMAND_NOT_FOUND",
"Select a leaf command from schema list.",
2,
));
}
let mut properties = Map::new();
let mut required = Vec::new();
for arg in selected.get_arguments() {
let id = arg.get_id().as_str();
if matches!(arg.get_action(), ArgAction::Help | ArgAction::Version) {
continue;
}
let mut property = crate::cli::input_schema::property(arg)?;
if let Some(help) = arg.get_help() {
property["description"] = json!(help.to_string());
}
if let Some(long) = arg.get_long() {
property["cliFlag"] = json!(format!("--{long}"));
} else {
property["positional"] = json!(true);
}
if let Some(default) = arg.get_default_values().first() {
let default = default.to_string_lossy();
property["default"] = match property["type"].as_str() {
Some("integer") => json!(default.parse::<u64>().map_err(|_| AppError::protocol())?),
Some("boolean") => json!(default == "true"),
_ => json!(default),
};
}
if arg.is_required_set() {
required.push(json!(id));
}
properties.insert(id.to_owned(), property);
}
let writes = matches!(
path,
"message.send"
| "message.reply"
| "napcat.start"
| "napcat.stop"
| "napcat.restart"
| "napcat.install"
| "napcat.uninstall"
| "napcat.remove"
| "napcat.update"
| "napcat.rollback"
| "media.download"
| "media.prune"
| "skill.export"
| "message.export"
);
let mut input_schema = json!({"$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object",
"properties":properties,"required":required,"additionalProperties":false});
let constraints = crate::cli::rules::constraints(path, selected);
if !constraints.is_empty() {
input_schema["allOf"] = json!(constraints);
}
let response_schema = if path == "event.watch" {
crate::cli::schema::event_stream()
} else {
typed_response(path)?.ok_or_else(AppError::protocol)?
};
let stream = path == "event.watch";
Ok(Description {
name: path.into(),
input_schema: SchemaDocument::new(input_schema)?,
response_schema: SchemaDocument::new(response_schema)?,
side_effects: if writes {
"explicit operation; inspect task skill"
} else {
"read"
},
request_id: if matches!(path, "message.send" | "message.reply") {
"required unless dry-run"
} else if path == "napcat.start" {
"required for --new"
} else {
"not required"
},
defaults: Defaults {
automatic_instance_selection: Selection::OnlyOne,
automatic_write_retry: No,
},
output_format: if stream {
OutputFormat::Ndjson
} else {
OutputFormat::Json
},
pre_stream_error_schema: if stream {
Some(SchemaDocument::new(crate::cli::schema::error_envelope())?)
} else {
None
},
stream_protocol: if stream {
Some(StreamProtocol {
ready: "once after identity checks and connection",
pre_ready_failure: "single preStreamErrorSchema record",
normal_termination: "summary",
output_failure: "no further output; discard incomplete final line",
})
} else {
None
},
})
}
fn typed_response(path: &str) -> Result<Option<Value>> {
use crate::core::contracts::{
installation::{Installation, Installed, RecordedInstance, Uninstalled},
reply::Reply,
schema,
};
let schema = match path {
"schema.list" => schema::<Reply<Vec<CommandSummary>>>(),
"schema.get" => schema::<Reply<Description>>(),
"skill.list" => schema::<Reply<Vec<SkillSummary>>>(),
"skill.get" => schema::<Reply<SkillGuide>>(),
"skill.export" => schema::<Reply<SkillExport>>(),
"message.send" | "message.reply" => schema::<
crate::core::contracts::reply::ScopedReply<crate::core::contracts::write::WriteResult>,
>(),
"request.get" => schema::<crate::core::contracts::write::RequestReply>(),
"media.download" => schema::<
crate::core::contracts::reply::ScopedReply<crate::core::contracts::write::Download>,
>(),
"media.prune" => schema::<Reply<crate::core::contracts::write::Pruned>>(),
"message.history" | "message.search" => schema::<
crate::core::contracts::reply::PageReply<
Vec<crate::core::contracts::message::Message>,
crate::core::contracts::history::HistoryPage,
>,
>(),
"message.export" => schema::<crate::core::contracts::history::ExportReply>(),
"message.get" => schema::<
crate::core::contracts::reply::ScopedReply<crate::core::contracts::message::Message>,
>(),
"contact.get" => schema::<
crate::core::contracts::reply::ScopedReply<crate::core::contracts::resource::Contact>,
>(),
"group.get" => schema::<
crate::core::contracts::reply::ScopedReply<crate::core::contracts::resource::Group>,
>(),
"conversation.get" => schema::<
crate::core::contracts::reply::ScopedReply<crate::core::contracts::resource::Recent>,
>(),
"contact.list" => schema::<
crate::core::contracts::reply::PagedReply<
Vec<crate::core::contracts::resource::Contact>,
crate::core::contracts::resource::CollectionPage,
>,
>(),
"group.list" => schema::<
crate::core::contracts::reply::PagedReply<
Vec<crate::core::contracts::resource::Group>,
crate::core::contracts::resource::CollectionPage,
>,
>(),
"group.members" => schema::<
crate::core::contracts::reply::PagedReply<
Vec<crate::core::contracts::resource::Member>,
crate::core::contracts::resource::CollectionPage,
>,
>(),
"conversation.list" => schema::<
crate::core::contracts::reply::PagedReply<
Vec<crate::core::contracts::resource::Recent>,
crate::core::contracts::resource::CollectionPage,
>,
>(),
"doctor" => schema::<Reply<crate::core::contracts::diagnostic::Diagnostic>>(),
"status" => schema::<crate::core::contracts::instance::StatusReply>(),
"napcat.start" | "napcat.restart" | "napcat.update" | "napcat.rollback" => schema::<
crate::core::contracts::reply::ScopedReply<crate::core::contracts::instance::Started>,
>(),
"napcat.stop" => schema::<Reply<crate::core::contracts::instance::Stopped>>(),
"napcat.remove" => schema::<Reply<crate::core::contracts::instance::Removed>>(),
"napcat.install" => schema::<Reply<Installed>>(),
"napcat.uninstall" => schema::<Reply<Uninstalled>>(),
"napcat.installations" => schema::<Reply<Vec<Installation>>>(),
"napcat.instances" => schema::<Reply<Vec<RecordedInstance>>>(),
_ => return Ok(None),
};
serde_json::to_value(schema)
.map(Some)
.map_err(|_| AppError::storage())
}