#[allow(clippy::wildcard_imports)]
use super::*;
pub(super) fn create_project(_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 lead = opt_string(args, "lead")?;
let start_date = trimmed_optional(opt_string(args, "start_date")?);
let due_date = trimmed_optional(opt_string(args, "due_date")?);
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::projects::NewProject {
name,
description,
status,
lead,
start_date,
due_date,
};
match crate::projects::create_project(&mut config, &new) {
Ok(project) => {
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::projects::project_display(&project.id, &project.name)
),
project_value(&project),
))
}
Err(error) => Ok(project_refusal(error)),
}
}
pub(super) fn delete_project(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "project")?;
if opt_bool(args, "confirm")? != Some(true) {
return Ok(error_result(
"invalid input: `confirm` must be true to delete a project — only the registry \
entry is removed and no issue 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))),
};
match crate::projects::delete_project(&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(member_value).collect();
let text = format!(
"Deleted project {}\n{}",
crate::projects::project_display(&deletion.id, &deletion.name),
deletion_notice(&deletion),
);
Ok(success_result(
text,
json!({
"id": deletion.id.as_str(),
"name": deletion.name,
"affected": affected,
}),
))
}
Err(error) => Ok(project_refusal(error)),
}
}
pub(super) fn get_project(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "project")?;
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 registry = store_with_config(store, config);
let reports = match crate::commands::view::project_reports(®istry, &issues) {
Ok((_, reports)) => reports,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
match crate::projects::resolve(®istry.config, &issues, &reference) {
Ok(project) => Ok(success_result(
project_record_text(&project, reports.get(&project.id)),
project_value(&project),
)),
Err(error) => Ok(project_refusal(error)),
}
}
pub(super) fn list_projects(_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 projects = crate::projects::list_projects(&config, &issues);
let structured: Vec<Value> = projects.iter().map(project_record_value).collect();
Ok(success_result(
project_rows_text(&projects),
json!({ "projects": structured }),
))
}
pub(super) fn project_associate_issues(
_config: &Config,
store: &Store,
args: &Value,
) -> ToolOutcome {
let reference = req_string(args, "project")?;
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 registry = store_with_config(store, config);
let id = match crate::projects::resolve_project_id(®istry.config, &reference) {
Ok(id) => id,
Err(error) => return Ok(project_refusal(error)),
};
match registry.bulk_associate(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(association_outcome_value).collect();
let changes: Vec<String> = outcomes
.iter()
.map(|outcome| match &outcome.outcome {
Ok(applied) if applied.changed => {
format!("{}: associated", outcome.selector)
}
Ok(_) => format!("{}: already associated", outcome.selector),
Err(error) => {
format!(
"{}: {}",
outcome.selector,
crate::error::render_error(error)
)
}
})
.collect();
let text = format!(
"associated {changed} of {} issue(s) with {id}\n{}",
outcomes.len(),
changes.join("\n")
);
Ok(success_result(
text,
json!({
"project_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 project_dissociate_issues(
_config: &Config,
store: &Store,
args: &Value,
) -> ToolOutcome {
let reference = req_string(args, "project")?;
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 registry = store_with_config(store, config);
let needle = crate::projects::resolve_project_id(®istry.config, &reference)
.map_or_else(|_| reference.clone(), |id| id.to_string());
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 registry.dissociate(selector, &needle) {
Ok(applied) => {
if applied.changed {
changed += 1;
changes.push(format!("{selector}: dissociated"));
} else {
changes.push(format!("{selector}: was not associated"));
}
rows.push(json!({
"selector": selector,
"changed": applied.changed,
"project_ids": applied.project_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!(
"dissociated {changed} of {} issue(s) from {needle}\n{}",
selectors.len(),
changes.join("\n")
);
Ok(success_result(
text,
json!({
"project_id": needle,
"changed": changed,
"failed": failed,
"total": selectors.len(),
"outcomes": rows,
}),
))
}
pub(super) fn project_membership(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "project")?;
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() {
Ok(issues) => issues,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
match crate::projects::membership(&config, &issues, &reference) {
Ok(membership) => {
let members: Vec<Value> = membership.members.iter().map(member_value).collect();
let text = format!(
"{}: {} member(s)",
membership.project_id, membership.member_count
);
Ok(success_result(
text,
json!({
"project_id": membership.project_id.as_str(),
"member_count": membership.member_count,
"members": members,
}),
))
}
Err(error) => Ok(project_refusal(error)),
}
}
pub(super) fn update_project(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "project")?;
let name = opt_string(args, "name")?;
let description = opt_string(args, "description")?;
let status = opt_string(args, "status")?;
let lead = opt_string(args, "lead")?;
let start_date = opt_string(args, "start_date")?;
let due_date = opt_string(args, "due_date")?;
let edit = crate::projects::ProjectEdit {
name: name.map_or(
crate::projects::Patch::Unchanged,
crate::projects::Patch::Set,
),
description: text_patch(description),
status: text_patch(status),
lead: text_patch(lead),
start_date: date_patch(start_date),
due_date: date_patch(due_date),
};
if edit.name.is_unchanged()
&& edit.description.is_unchanged()
&& edit.status.is_unchanged()
&& edit.lead.is_unchanged()
&& edit.start_date.is_unchanged()
&& edit.due_date.is_unchanged()
{
return Ok(error_result(crate::error::render_error(
&crate::commands::project_edit_requires_a_field(),
)));
}
let (mut config, path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
match crate::projects::edit_project(&mut config, &reference, &edit) {
Ok(project) => {
if let Err(e) = save_registry_config(&mut config, &path) {
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))),
};
match crate::projects::resolve(&config, &issues, project.id.as_str()) {
Ok(project) => Ok(success_result(
format!(
"Updated {}",
crate::projects::project_display(&project.id, &project.name)
),
project_value(&project),
)),
Err(error) => Ok(project_refusal(error)),
}
}
Err(error) => Ok(project_refusal(error)),
}
}
pub(super) fn dispatch(
config: &Config,
store: &Store,
name: &str,
arguments: &Value,
) -> Option<ToolOutcome> {
match name {
"list_projects" => Some(list_projects(config, store, arguments)),
"get_project" => Some(get_project(config, store, arguments)),
"create_project" => Some(create_project(config, store, arguments)),
"update_project" => Some(update_project(config, store, arguments)),
"delete_project" => Some(delete_project(config, store, arguments)),
"project_membership" => Some(project_membership(config, store, arguments)),
"project_associate_issues" => Some(project_associate_issues(config, store, arguments)),
"project_dissociate_issues" => Some(project_dissociate_issues(config, store, arguments)),
_ => None,
}
}