use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::error::{MyceliumError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LinearConfig {
pub api_key: String,
pub team_id: String,
pub team_name: String,
pub sync_enabled: bool,
#[serde(default = "default_repo_label_mode")]
pub repo_label: String,
#[serde(default = "default_branch_label_mode")]
pub branch_label: String,
#[serde(default)]
pub extra_labels: Vec<String>,
#[serde(default)]
pub default_assignee_id: Option<String>,
#[serde(default)]
pub mapping: MappingConfig,
}
fn default_repo_label_mode() -> String {
"auto".to_string()
}
fn default_branch_label_mode() -> String {
"auto".to_string()
}
impl LinearConfig {
pub fn resolve_filter_labels(&self) -> Vec<String> {
let mut labels = Vec::new();
let repo = if self.repo_label == "auto" {
detect_repo_name()
} else if self.repo_label.is_empty() || self.repo_label == "none" {
None
} else {
Some(self.repo_label.clone())
};
if let Some(r) = repo {
labels.push(r);
}
let branch = if self.branch_label == "auto" {
detect_branch_name()
} else if self.branch_label.is_empty() || self.branch_label == "none" {
None
} else {
Some(self.branch_label.clone())
};
if let Some(b) = branch {
labels.push(b);
}
labels.extend(self.extra_labels.iter().cloned());
labels
}
}
fn detect_repo_name() -> Option<String> {
let output = std::process::Command::new("git")
.args(["remote", "get-url", "origin"])
.output()
.ok()?;
let url = String::from_utf8(output.stdout).ok()?.trim().to_string();
if !url.is_empty() {
let name = url
.rsplit('/')
.next()
.unwrap_or(&url)
.trim_end_matches(".git")
.to_string();
if !name.is_empty() {
return Some(name);
}
}
std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
}
fn detect_branch_name() -> Option<String> {
let output = std::process::Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.output()
.ok()?;
let branch = String::from_utf8(output.stdout).ok()?.trim().to_string();
if branch.is_empty() || branch == "HEAD" {
None
} else {
Some(branch)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MappingConfig {
#[serde(default = "default_epic_mode")]
pub epic_mode: String,
#[serde(default = "default_status_mapping")]
pub status: HashMap<String, String>,
#[serde(default = "default_priority_mapping")]
pub priority: HashMap<String, u8>,
#[serde(default)]
pub assignees: HashMap<String, String>, }
impl Default for MappingConfig {
fn default() -> Self {
Self {
epic_mode: default_epic_mode(),
status: default_status_mapping(),
priority: default_priority_mapping(),
assignees: HashMap::new(),
}
}
}
fn default_epic_mode() -> String {
"label".to_string()
}
fn default_status_mapping() -> HashMap<String, String> {
let mut m = HashMap::new();
m.insert("open".to_string(), "Todo".to_string());
m.insert("in_progress".to_string(), "In Progress".to_string());
m.insert("closed".to_string(), "Done".to_string());
m
}
fn default_priority_mapping() -> HashMap<String, u8> {
let mut m = HashMap::new();
m.insert("low".to_string(), 4);
m.insert("medium".to_string(), 3);
m.insert("high".to_string(), 2);
m.insert("critical".to_string(), 1);
m
}
impl LinearConfig {
pub fn config_dir() -> PathBuf {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(".mycelium")
.join(".linear")
}
pub fn config_path() -> PathBuf {
Self::config_dir().join("config.toml")
}
pub fn load() -> Result<Self> {
let path = Self::config_path();
if !path.exists() {
return Err(MyceliumError::LinearConfig(
"Linear not configured. Run `myc linear setup` first".to_string(),
));
}
let content = std::fs::read_to_string(&path)?;
toml::from_str(&content).map_err(|e| MyceliumError::LinearConfig(e.to_string()))
}
pub fn save(&self) -> Result<()> {
let dir = Self::config_dir();
std::fs::create_dir_all(&dir)?;
let gitignore_path = dir.parent().unwrap().join(".gitignore");
if gitignore_path.exists() {
let content = std::fs::read_to_string(&gitignore_path)?;
if !content.contains(".linear") {
let mut f = std::fs::OpenOptions::new()
.append(true)
.open(&gitignore_path)?;
use std::io::Write;
writeln!(f, ".linear/")?;
}
} else {
std::fs::write(&gitignore_path, ".linear/\n")?;
}
let content =
toml::to_string_pretty(self).map_err(|e| MyceliumError::LinearConfig(e.to_string()))?;
std::fs::write(Self::config_path(), content)?;
Ok(())
}
pub fn exists() -> bool {
Self::config_path().exists()
}
pub fn remove() -> Result<()> {
let dir = Self::config_dir();
if dir.exists() {
std::fs::remove_dir_all(&dir)?;
}
Ok(())
}
}