#[allow(clippy::wildcard_imports)]
use super::*;
pub(super) fn template_management_config() -> TixResult<(Config, std::path::PathBuf)> {
let cwd = std::env::current_dir().map_err(crate::error::TixError::Io)?;
let root = Config::project_root(&cwd).ok_or_else(|| {
crate::error::TixError::Config("no tix.toml found — run \"tix init\" first".into())
})?;
let path = root.join("tix.toml");
let config = Config::load(&root)?;
Ok((config, path))
}
pub(super) fn template_record_value(id: &str, template: &Template) -> Value {
json!({
"id": id,
"name": template.display_name(id),
"scope": template.scope.to_string(),
"defaults": template.defaults,
"prompts": template.prompts,
"references": template.references,
"extensions": template.extensions,
})
}
pub(super) fn template_value(detail: &TemplateDetail) -> Value {
template_record_value(detail.id(), detail.template())
}
pub(super) fn template_registry_value(config: &Config, id: &str) -> Option<Value> {
config
.issue_templates
.get(id)
.map(|template| template_record_value(id, template))
}
pub(super) fn template_outcome_value(
mutation: &str,
status: &str,
template: Option<Value>,
diagnostics: Vec<Value>,
) -> Value {
json!({
"status": status,
"mutation": mutation,
"template": template,
"diagnostics": diagnostics,
})
}
pub(super) fn template_error_result(text: String, structured: Value) -> Value {
json!({
"content": [{ "type": "text", "text": text }],
"structuredContent": structured,
"isError": true,
})
}
pub(super) fn template_reject(mutation: &str, field: &str, message: String) -> ToolOutcome {
let text = format!("invalid input: `{field}`: {message}");
let structured = template_outcome_value(
mutation,
"rejected",
None,
vec![json!({
"kind": "invalid-payload",
"field": field,
"message": message,
})],
);
Ok(template_error_result(text, structured))
}
pub(super) fn template_write_refusal(mutation: &str, error: &TemplateWriteError) -> Value {
let status = if error.is_refusal() {
"rejected"
} else {
"failed"
};
let message = crate::error::render_error(error.inner());
let structured = template_outcome_value(
mutation,
status,
None,
vec![json!({
"kind": status,
"field": refusal_field(&message),
"message": message.clone(),
})],
);
template_error_result(message, structured)
}
pub(super) fn refusal_field(message: &str) -> &str {
message
.split('`')
.skip(1)
.step_by(2)
.find(|span| span.starts_with("issue_templates"))
.unwrap_or("issue_templates")
}
pub(super) fn template_definition(
args: &Value,
mutation: &str,
) -> ToolOutcome2<TemplateDefinition> {
let name = opt_string_step(args, "name")?;
let scope = match opt_string_step(args, "scope")? {
Some(raw) => match raw.parse::<TemplateScope>() {
Ok(scope) => Some(scope),
Err(message) => return Err(template_reject(mutation, "scope", message)),
},
None => None,
};
let defaults = template_block::<TemplateDefaults>(args, "defaults", mutation)?;
let prompts = template_items::<TemplatePrompt>(args, "prompts", mutation)?;
let references = template_items::<TemplateReference>(args, "references", mutation)?;
let mut definition = match (name, scope) {
(Some(name), Some(scope)) => TemplateDefinition::new(name, scope),
(Some(name), None) => TemplateDefinition::rename(name),
(None, Some(scope)) => TemplateDefinition::unnamed(scope),
(None, None) => TemplateDefinition::default(),
};
if let Some(defaults) = defaults {
definition = definition.with_defaults(defaults);
}
if let Some(prompts) = prompts {
definition = definition.with_prompts(prompts);
}
if let Some(references) = references {
definition = definition.with_references(references);
}
Ok(definition)
}
pub(super) fn opt_string_step(args: &Value, key: &str) -> ToolOutcome2<Option<String>> {
opt_string(args, key).map_err(|()| proto())
}
pub(super) fn template_block<T: serde::de::DeserializeOwned>(
args: &Value,
key: &str,
mutation: &str,
) -> ToolOutcome2<Option<T>> {
match args.get(key) {
None | Some(Value::Null) => Ok(None),
Some(value) => match serde_json::from_value::<T>(value.clone()) {
Ok(block) => Ok(Some(block)),
Err(error) => Err(template_reject(mutation, key, error.to_string())),
},
}
}
pub(super) fn template_items<T: serde::de::DeserializeOwned>(
args: &Value,
key: &str,
mutation: &str,
) -> ToolOutcome2<Option<Vec<T>>> {
let Some(value) = args.get(key) else {
return Ok(None);
};
if value.is_null() {
return Ok(None);
}
let Some(items) = value.as_array() else {
return Err(proto());
};
let mut parsed = Vec::with_capacity(items.len());
for (index, item) in items.iter().enumerate() {
match serde_json::from_value::<T>(item.clone()) {
Ok(value) => parsed.push(value),
Err(error) => {
return Err(template_reject(
mutation,
&format!("{key}[{index}]"),
error.to_string(),
));
}
}
}
Ok(Some(parsed))
}
pub(super) fn list_text(
issues: &[Issue],
group: Option<&str>,
resolution: &crate::workflow::CategoryResolution,
) -> String {
list::render_default_table(issues, group, resolution, 0)
}
pub(super) fn status_text(
config: &Config,
store: &Store,
issues: &[Issue],
summary: &status::BoardSummary,
show_controls: bool,
group: Option<&str>,
) -> String {
status::render(config, store, issues, summary, show_controls, group, None)
}
pub(super) const MAX_LINE_WIDTH: usize = 200;
pub(super) fn clipped(line: &str) -> String {
if line.chars().count() <= MAX_LINE_WIDTH {
return line.to_string();
}
let head: String = line.chars().take(MAX_LINE_WIDTH).collect();
format!("{head}…")
}
pub(super) fn search_text(hits: &[SearchHit]) -> String {
if hits.is_empty() {
return "No issues found.\n".to_string();
}
let mut out = String::new();
for hit in hits {
out.push_str(&format!(
"#{} {} — {} ({})\n",
hit.issue.number, hit.issue.title, hit.issue.status, hit.issue.filename
));
for line in &hit.matched_lines {
out.push_str(&format!(" {}\n", clipped(line)));
}
}
out
}
pub(super) fn sort_issues(issues: &mut [Issue], sort: Option<&str>) {
match sort.unwrap_or("created") {
"due" => issues.sort_by(|a, b| a.due_date.cmp(&b.due_date).then(a.number.cmp(&b.number))),
"start" => issues.sort_by(|a, b| {
a.start_date
.cmp(&b.start_date)
.then(a.number.cmp(&b.number))
}),
"reviewer" => {
issues.sort_by(|a, b| a.reviewer.cmp(&b.reviewer).then(a.number.cmp(&b.number)))
}
"cycle" => issues.sort_by(|a, b| {
a.cycles
.as_ref()
.and_then(|v| v.first())
.cmp(&b.cycles.as_ref().and_then(|v| v.first()))
.then(a.number.cmp(&b.number))
}),
"milestone" => issues.sort_by(|a, b| {
a.milestones
.as_ref()
.and_then(|v| v.first())
.cmp(&b.milestones.as_ref().and_then(|v| v.first()))
.then(a.number.cmp(&b.number))
}),
"status" => issues.sort_by(|a, b| {
a.status
.display()
.cmp(b.status.display())
.then(a.number.cmp(&b.number))
}),
"priority" => issues.sort_by(|a, b| {
a.priority
.to_string()
.cmp(&b.priority.to_string())
.then(a.number.cmp(&b.number))
}),
_ => issues.sort_by(|a, b| a.created.cmp(&b.created).then(a.number.cmp(&b.number))),
}
}
pub(super) fn lint_text(n_issues: usize, findings: &[lint::TixIssueFinding]) -> String {
let errors = findings
.iter()
.filter(|f| f.severity == lint::Severity::Error)
.count();
if errors == 0 {
let warnings = findings.len();
return if warnings == 0 {
format!("Lint OK: {n_issues} issues checked.\n")
} else {
format!(
"Lint OK with {warnings} warning{}: {n_issues} issues checked.\n",
if warnings == 1 { "" } else { "s" }
)
};
}
let mut out = String::new();
for f in findings
.iter()
.filter(|f| f.severity == lint::Severity::Error)
{
out.push_str(&format!(
"{}:{}:{} {} {} {}\n",
f.file, f.line, f.column, f.kind, f.value, f.message
));
}
out
}
pub(super) fn resolve_filename(store: &Store, raw: &str) -> TixResult<String> {
let selector = parse_selector(store, raw)?;
let issue = store.get(selector)?;
Ok(issue.filename)
}