pub mod github_issues;
pub mod jira;
use anyhow::Result;
use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use crate::config::{ParsecConfig, TrackerProvider};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ticket {
pub id: String,
pub title: String,
pub status: Option<String>,
pub assignee: Option<String>,
pub url: Option<String>,
}
pub fn load_atlassian_env() {
let env_path: PathBuf = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".claude")
.join(".atlassian-env");
if let Ok(contents) = std::fs::read_to_string(&env_path) {
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
let value = value.trim();
let allowed_prefixes = ["JIRA_", "PARSEC_", "CONFLUENCE_", "ATLASSIAN_"];
if !allowed_prefixes.iter().any(|p| key.starts_with(p)) {
eprintln!("warning: ignoring disallowed env var '{}' in .atlassian-env (allowed prefixes: JIRA_, PARSEC_, CONFLUENCE_, ATLASSIAN_)", key);
continue;
}
if std::env::var(key).is_err() {
std::env::set_var(key, value);
}
}
}
}
}
pub async fn fetch_ticket(
config: &ParsecConfig,
id: &str,
repo_root: Option<&Path>,
) -> Result<Option<Ticket>> {
load_atlassian_env();
match config.tracker.provider {
TrackerProvider::Jira => fetch_jira_ticket(config, id).await,
TrackerProvider::Github => {
let tracker = github_issues::GithubIssueTracker::new(repo_root, config);
let ticket = tracker.fetch_ticket(id).await?;
Ok(Some(ticket))
}
TrackerProvider::Gitlab | TrackerProvider::None => {
if std::env::var(crate::env::JIRA_BASE_URL).is_ok()
&& (std::env::var(crate::env::JIRA_PAT).is_ok()
|| std::env::var(crate::env::PARSEC_JIRA_TOKEN).is_ok())
{
if let Ok(Some(ticket)) = fetch_jira_ticket(config, id).await {
return Ok(Some(ticket));
}
}
if let Some(root) = repo_root {
if let Ok(remote_url) = crate::git::get_remote_url(root) {
if crate::github::parse_github_remote(&remote_url).is_some() {
let clean_id = id.trim_start_matches('#');
if clean_id.chars().all(|c| c.is_ascii_digit()) {
let tracker = github_issues::GithubIssueTracker::new(repo_root, config);
if let Ok(ticket) = tracker.fetch_ticket(id).await {
return Ok(Some(ticket));
}
}
}
}
}
Ok(None)
}
}
}
pub async fn try_transition(config: &ParsecConfig, ticket: &str, target_status: &str) {
if !matches!(
config.tracker.provider,
TrackerProvider::Jira | TrackerProvider::None
) {
return;
}
load_atlassian_env();
let base_url = config
.tracker
.jira
.as_ref()
.map(|j| j.base_url.clone())
.or_else(|| std::env::var(crate::env::JIRA_BASE_URL).ok());
let base_url = match base_url {
Some(url) => url,
None => return,
};
let email = config.tracker.jira.as_ref().and_then(|j| j.email.clone());
let jira = jira::JiraTracker::new(&base_url, email.as_deref());
match jira.transition_issue(ticket, target_status).await {
Ok(()) => {
eprintln!(" {} Ticket status → {}", "✓".green(), target_status);
}
Err(e) => {
eprintln!(" warning: failed to transition ticket: {e}");
}
}
}
pub async fn post_comment(
config: &ParsecConfig,
id: &str,
body: &str,
repo_root: Option<&Path>,
) -> Result<()> {
load_atlassian_env();
match config.tracker.provider {
TrackerProvider::Jira => {
let base_url = config
.tracker
.jira
.as_ref()
.map(|j| j.base_url.clone())
.or_else(|| std::env::var(crate::env::JIRA_BASE_URL).ok())
.ok_or_else(|| {
anyhow::anyhow!(
"Jira base URL not found. Set it in config or {} env var.",
crate::env::JIRA_BASE_URL,
)
})?;
let email = config.tracker.jira.as_ref().and_then(|j| j.email.clone());
let tracker = jira::JiraTracker::new(&base_url, email.as_deref());
tracker.add_comment(id, body).await
}
TrackerProvider::Github => {
let tracker = github_issues::GithubIssueTracker::new(repo_root, config);
tracker.add_comment(id, body).await
}
TrackerProvider::Gitlab | TrackerProvider::None => {
if std::env::var(crate::env::JIRA_BASE_URL).is_ok()
&& (std::env::var(crate::env::JIRA_PAT).is_ok()
|| std::env::var(crate::env::PARSEC_JIRA_TOKEN).is_ok())
{
let base_url = std::env::var(crate::env::JIRA_BASE_URL)?;
let email = config.tracker.jira.as_ref().and_then(|j| j.email.clone());
let tracker = jira::JiraTracker::new(&base_url, email.as_deref());
if tracker.add_comment(id, body).await.is_ok() {
return Ok(());
}
}
if let Some(root) = repo_root {
if let Ok(remote_url) = crate::git::get_remote_url(root) {
if crate::github::parse_github_remote(&remote_url).is_some() {
let clean_id = id.trim_start_matches('#');
if clean_id.chars().all(|c| c.is_ascii_digit()) {
let tracker = github_issues::GithubIssueTracker::new(repo_root, config);
return tracker.add_comment(id, body).await;
}
}
}
}
anyhow::bail!(
"No tracker configured to post comments. \
Set tracker.provider in config or configure environment variables."
)
}
}
}
async fn fetch_jira_ticket(config: &ParsecConfig, id: &str) -> Result<Option<Ticket>> {
let base_url = config
.tracker
.jira
.as_ref()
.map(|j| j.base_url.clone())
.or_else(|| std::env::var(crate::env::JIRA_BASE_URL).ok())
.ok_or_else(|| {
anyhow::anyhow!(
"Jira base URL not found. Set it in config or {} env var.",
crate::env::JIRA_BASE_URL,
)
})?;
let email = config.tracker.jira.as_ref().and_then(|j| j.email.clone());
let tracker = jira::JiraTracker::new(&base_url, email.as_deref());
let ticket = tracker.fetch_ticket(id).await?;
Ok(Some(ticket))
}