#[allow(clippy::wildcard_imports)]
use super::*;
pub(super) fn create_epic(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let name = req_string(args, "name")?;
let description = opt_string(args, "description")?;
let status = opt_string(args, "status")?;
let owner = opt_string(args, "owner")?;
let start_date = trimmed_optional(opt_string(args, "start_date")?);
let due_date = trimmed_optional(opt_string(args, "due_date")?);
let milestone = trimmed_optional(opt_string(args, "milestone")?);
let cycle = trimmed_optional(opt_string(args, "cycle")?);
let weight = match epic_create_weight(args, "weight") {
Ok(weight) => weight,
Err(outcome) => return outcome,
};
let projects = opt_string(args, "projects")?
.map(|raw| crate::commands::parse_csv_list(&raw))
.unwrap_or_default();
let (mut config, path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let new = crate::epics::NewEpic {
name,
description,
status,
owner,
start_date,
due_date,
weight,
projects,
milestone,
cycle,
};
match crate::epics::create_epic(&mut config, &new) {
Ok(epic) => {
if let Err(e) = save_registry_config(&mut config, &path) {
return Ok(error_result(crate::error::render_error(&e)));
}
Ok(success_result(
format!(
"Created {}",
crate::epics::epic_display(&epic.id, &epic.name)
),
epic_record_value(&epic),
))
}
Err(error) => Ok(epic_refusal(error)),
}
}
pub(super) fn delete_epic(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "epic")?;
if opt_bool(args, "confirm")? != Some(true) {
return Ok(error_result(
"invalid input: `confirm` must be true to delete an epic — only the registry \
entry is removed and neither an issue nor a child epic is deleted"
.to_string(),
));
}
let (mut config, path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let issues = match store.list_all() {
Ok(issues) => issues,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let epic = match crate::epics::resolve(&config, &issues, &reference) {
Ok(epic) => epic,
Err(error) => return Ok(epic_refusal(error)),
};
match crate::epics::delete_epic(&mut config, &reference, &issues) {
Ok(deletion) => {
if let Err(e) = save_registry_config(&mut config, &path) {
return Ok(error_result(crate::error::render_error(&e)));
}
let affected: Vec<Value> = deletion.affected.iter().map(epic_member_value).collect();
let children: Vec<Value> = deletion.child_epics.iter().map(epic_ref_value).collect();
let text = format!(
"Deleted epic {}\n{}",
crate::epics::epic_display(&deletion.id, &deletion.name),
cmd_epic::impact_notice(&epic),
);
Ok(success_result(
text,
json!({
"id": deletion.id.as_str(),
"name": deletion.name,
"affected": affected,
"child_epics": children,
}),
))
}
Err(error) => Ok(epic_refusal(error)),
}
}
pub(super) fn epic_associate_issues(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "epic")?;
let selectors = crate::commands::parse_csv_list(&req_string(args, "issues")?);
if selectors.is_empty() {
return Ok(error_result(
"invalid input: `issues` must name at least one issue selector".to_string(),
));
}
let (config, _path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let store = store_with_config(store, config);
let id = match crate::epics::resolve_epic_id(&store.config.epics, &reference) {
Ok(id) => id,
Err(error) => return Ok(epic_refusal(error)),
};
match store.bulk_reference(id.as_str(), &selectors) {
Ok(outcomes) => {
let changed = outcomes
.iter()
.filter(|outcome| outcome.outcome.as_ref().is_ok_and(|a| a.changed))
.count();
let failed = outcomes
.iter()
.filter(|outcome| outcome.outcome.is_err())
.count();
let rows: Vec<Value> = outcomes.iter().map(epic_reference_outcome_value).collect();
let changes: Vec<String> = outcomes
.iter()
.map(|outcome| match &outcome.outcome {
Ok(applied) if applied.changed => {
format!("{}: referenced", outcome.selector)
}
Ok(_) => format!("{}: already referenced", outcome.selector),
Err(error) => {
format!(
"{}: {}",
outcome.selector,
crate::error::render_error(error)
)
}
})
.collect();
let text = format!(
"referenced {changed} of {} issue(s) by {id}\n{}",
outcomes.len(),
changes.join("\n")
);
Ok(success_result(
text,
json!({
"epic_id": id.as_str(),
"changed": changed,
"failed": failed,
"total": outcomes.len(),
"outcomes": rows,
}),
))
}
Err(error) => Ok(error_result(crate::error::render_error(&error))),
}
}
pub(super) fn epic_dissociate_issues(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "epic")?;
let selectors = crate::commands::parse_csv_list(&req_string(args, "issues")?);
if selectors.is_empty() {
return Ok(error_result(
"invalid input: `issues` must name at least one issue selector".to_string(),
));
}
let (config, _path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let store = store_with_config(store, config);
let needle = crate::epics::dereference_needle(&store.config.epics, &reference);
let mut rows: Vec<Value> = Vec::with_capacity(selectors.len());
let mut changes: Vec<String> = Vec::with_capacity(selectors.len());
let mut changed = 0usize;
let mut failed = 0usize;
for selector in &selectors {
match store.dereference_epic(selector, &needle) {
Ok(applied) => {
if applied.changed {
changed += 1;
changes.push(format!("{selector}: dereferenced"));
} else {
changes.push(format!("{selector}: was not referenced"));
}
rows.push(json!({
"selector": selector,
"changed": applied.changed,
"epic_ids": applied.epic_ids,
}));
}
Err(error) => {
failed += 1;
changes.push(format!(
"{selector}: {}",
crate::error::render_error(&error)
));
rows.push(
json!({ "selector": selector, "error": crate::error::render_error(&error) }),
);
}
}
}
let text = format!(
"dereferenced {changed} of {} issue(s) from {needle}\n{}",
selectors.len(),
changes.join("\n")
);
Ok(success_result(
text,
json!({
"epic_id": needle,
"changed": changed,
"failed": failed,
"total": selectors.len(),
"outcomes": rows,
}),
))
}
pub(super) fn epic_membership(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "epic")?;
let (config, _path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let store = store_with_config(store, config);
let issues = match store.list_all() {
Ok(issues) => issues,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
match crate::epics::membership(&store.config, &issues, &reference) {
Ok(membership) => {
let members: Vec<Value> = membership.members.iter().map(epic_member_value).collect();
let text = format!(
"{}: {} member(s)",
membership.epic_id, membership.member_count
);
Ok(success_result(
text,
json!({
"epic_id": membership.epic_id.as_str(),
"member_count": membership.member_count,
"members": members,
}),
))
}
Err(error) => Ok(epic_refusal(error)),
}
}
pub(super) fn get_epic(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "epic")?;
let (config, _path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let store = store_with_config(store, config);
let issues = match store.list_all() {
Ok(issues) => issues,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let index = crate::epics::EpicIndex::build(&store.config, &issues);
let epic = match index.resolve(&reference) {
Ok(epic) => epic.clone(),
Err(error) => return Ok(epic_refusal(error)),
};
let targets = match epic_reference_targets(&store.config, &store) {
Ok(targets) => targets,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let (graph_issues, done_column) = match store.graph_source() {
Ok(source) => source,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let graph = crate::commands::graph_over(&graph_issues, &done_column);
let rollup = match index.rollup(&store.config, &reference, &targets, &graph) {
Ok(rollup) => rollup,
Err(error) => return Ok(epic_refusal(error)),
};
let mut value = epic_record_value(&epic);
value["rollup"] = epic_rollup_value(&rollup);
Ok(success_result(
cmd_epic::record_lines(&epic, Some(&rollup)).join("\n"),
value,
))
}
pub(super) fn list_epics(_config: &Config, store: &Store, _args: &Value) -> ToolOutcome {
let (config, _path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let issues = match store.list_all() {
Ok(issues) => issues,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let index = crate::epics::EpicIndex::build(&config, &issues);
let structured: Vec<Value> = index.epics().into_iter().map(epic_record_value).collect();
let missing: Vec<Value> = index.missing().iter().map(epic_missing_value).collect();
Ok(success_result(
cmd_epic::listing_lines(&config, &issues).join("\n"),
json!({ "epics": structured, "missing_references": missing }),
))
}
pub(super) fn update_epic(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "epic")?;
let name = opt_string(args, "name")?;
let description = opt_string(args, "description")?;
let status = opt_string(args, "status")?;
let owner = opt_string(args, "owner")?;
let start_date = opt_string(args, "start_date")?;
let due_date = opt_string(args, "due_date")?;
let parent = opt_string(args, "parent")?;
let projects = opt_string(args, "projects")?;
let milestone = opt_string(args, "milestone")?;
let cycle = opt_string(args, "cycle")?;
let weight = match epic_weight_patch(args, "weight") {
Ok(weight) => weight,
Err(outcome) => return outcome,
};
let has_weight = !matches!(args.get("weight"), None | Some(Value::Null));
if name.is_none()
&& description.is_none()
&& status.is_none()
&& owner.is_none()
&& start_date.is_none()
&& due_date.is_none()
&& parent.is_none()
&& projects.is_none()
&& milestone.is_none()
&& cycle.is_none()
&& !has_weight
{
return Ok(error_result(epic_edit_requires_a_field()));
}
let projects_patch = match projects {
None => crate::epics::Patch::Unchanged,
Some(value) if value.trim().is_empty() => crate::epics::Patch::Clear,
Some(value) => crate::epics::Patch::Set(crate::commands::parse_csv_list(&value)),
};
let edit = crate::epics::EpicEdit {
name: name.map_or(crate::epics::Patch::Unchanged, crate::epics::Patch::Set),
description: text_patch(description),
status: text_patch(status),
owner: text_patch(owner),
start_date: date_patch(start_date),
due_date: date_patch(due_date),
weight,
parent: text_patch(parent),
projects: projects_patch,
milestone: text_patch(milestone),
cycle: text_patch(cycle),
};
let (mut config, path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
match crate::epics::edit_epic(&mut config, &reference, &edit) {
Ok(epic) => {
if let Err(e) = save_registry_config(&mut config, &path) {
return Ok(error_result(crate::error::render_error(&e)));
}
let store = store_with_config(store, config);
let issues = match store.list_all() {
Ok(issues) => issues,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let index = crate::epics::EpicIndex::build(&store.config, &issues);
match index.resolve(epic.id.as_str()) {
Ok(epic) => Ok(success_result(
format!(
"Updated {}",
crate::epics::epic_display(&epic.id, &epic.name)
),
epic_record_value(epic),
)),
Err(error) => Ok(epic_refusal(error)),
}
}
Err(error) => Ok(epic_refusal(error)),
}
}
pub(super) fn dispatch(
config: &Config,
store: &Store,
name: &str,
arguments: &Value,
) -> Option<ToolOutcome> {
match name {
"list_epics" => Some(list_epics(config, store, arguments)),
"get_epic" => Some(get_epic(config, store, arguments)),
"create_epic" => Some(create_epic(config, store, arguments)),
"update_epic" => Some(update_epic(config, store, arguments)),
"delete_epic" => Some(delete_epic(config, store, arguments)),
"epic_membership" => Some(epic_membership(config, store, arguments)),
"epic_associate_issues" => Some(epic_associate_issues(config, store, arguments)),
"epic_dissociate_issues" => Some(epic_dissociate_issues(config, store, arguments)),
_ => None,
}
}