use std::fs;
use std::io::IsTerminal;
use anyhow::{Context, Result};
use dialoguer::Input;
use tracing::{debug, info, instrument};
use super::types::CreateResult;
use crate::provider::CliTokenProvider;
use aptu_core::github::parse_owner_repo;
use aptu_core::{format_issue, post_issue};
#[instrument(skip_all, fields(repo = %repo, dry_run = %dry_run))]
pub async fn run(
repo: String,
title: Option<String>,
body: Option<String>,
from: Option<String>,
dry_run: bool,
) -> Result<CreateResult> {
let (owner, repo_name) = parse_owner_repo(&repo)?;
debug!(owner = %owner, repo = %repo_name, "Parsed repository");
let (final_title, final_body) = if let Some(file_path) = &from {
let content = fs::read_to_string(file_path)
.with_context(|| format!("Failed to read file: {file_path}"))?;
debug!(file_path = %file_path, "Read content from file");
let t = title.clone().unwrap_or_else(|| "Untitled".to_string());
let b = body.clone().unwrap_or(content);
(t, b)
} else if let (Some(t), Some(b)) = (&title, &body) {
(t.clone(), b.clone())
} else if let Some(t) = &title {
let b = prompt_body()?;
(t.clone(), b)
} else if let Some(b) = &body {
let t = prompt_title()?;
(t, b.clone())
} else {
let t = prompt_title()?;
let b = prompt_body()?;
(t, b)
};
debug!(title = %final_title, body_len = final_body.len(), "Collected issue content");
let ai_config = load_ai_config()?;
let provider = CliTokenProvider;
let ai_response = format_issue(&provider, &final_title, &final_body, &repo, &ai_config)
.await
.context("Failed to format issue with AI")?;
debug!(
formatted_title = %ai_response.formatted_title,
labels_count = ai_response.suggested_labels.len(),
"AI formatting complete"
);
if dry_run {
info!("Dry run mode: skipping GitHub API call");
return Ok(CreateResult {
issue_url: format!("https://github.com/{owner}/{repo_name}/issues/[preview]"),
issue_number: 0,
title: ai_response.formatted_title,
body: ai_response.formatted_body,
suggested_labels: ai_response.suggested_labels,
dry_run: true,
});
}
let (issue_url, issue_number) = post_issue(
&provider,
&owner,
&repo_name,
&ai_response.formatted_title,
&ai_response.formatted_body,
)
.await
.context("Failed to create GitHub issue")?;
info!(issue_number = issue_number, "Issue created successfully");
Ok(CreateResult {
issue_url,
issue_number,
title: ai_response.formatted_title,
body: ai_response.formatted_body,
suggested_labels: ai_response.suggested_labels,
dry_run: false,
})
}
fn load_ai_config() -> Result<aptu_core::config::AiConfig> {
let config = aptu_core::load_config().context("Failed to load configuration")?;
Ok(config.ai)
}
fn prompt_title() -> Result<String> {
if !std::io::stdin().is_terminal() {
anyhow::bail!("Interactive mode requires a terminal. Use --title flag instead.");
}
let title = Input::<String>::new()
.with_prompt("Issue title")
.validate_with(|input: &String| {
if input.is_empty() {
Err("Title cannot be empty")
} else if input.len() > 256 {
Err("Title must be 256 characters or less")
} else {
Ok(())
}
})
.interact()
.context("Failed to read title from input")?;
Ok(title)
}
fn prompt_body() -> Result<String> {
if !std::io::stdin().is_terminal() {
anyhow::bail!("Interactive mode requires a terminal. Use --body flag instead.");
}
let body = Input::<String>::new()
.with_prompt("Issue description (press Enter twice to finish, or use --body for multiline)")
.allow_empty(false)
.interact()
.context("Failed to read body from input")?;
Ok(body)
}