mirror of
https://github.com/jnsahaj/lumen.git
synced 2025-08-04 10:59:29 +00:00
Merge pull request #13 from jmattaa/feature/proj-config
Add project configuaration.
This commit is contained in:
commit
ec15a678e8
11 changed files with 273 additions and 85 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1,3 @@
|
|||
/target
|
||||
|
||||
lumen.config.json
|
||||
|
|
|
@ -15,6 +15,7 @@ lumen is a command-line tool that uses AI to generate commit messages, summarise
|
|||
- Generate commit message for staged changes
|
||||
- Generate summary for changes in a git commit by providing its [SHA-1](https://graphite.dev/guides/git-hash)
|
||||
- Generate summary for changes in git diff (staged/unstaged)
|
||||
- Ask questions about a specific change
|
||||
- Fuzzy-search for the commit to generate a summary
|
||||
- Free and unlimited - no API key required to work out of the box
|
||||
- Pretty output formatting enabled by Markdown
|
||||
|
|
|
@ -77,20 +77,6 @@ impl AIPrompt {
|
|||
return Err(AIPromptError("`draft` is only supported for diffs".into()));
|
||||
};
|
||||
|
||||
let conventional_types = r#"{
|
||||
"docs": "Documentation only changes",
|
||||
"style": "Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)",
|
||||
"refactor": "A code change that neither fixes a bug nor adds a feature",
|
||||
"perf": "A code change that improves performance",
|
||||
"test": "Adding missing tests or correcting existing tests",
|
||||
"build": "Changes that affect the build system or external dependencies",
|
||||
"ci": "Changes to our CI configuration files and scripts",
|
||||
"chore": "Other changes that don't modify src or test files",
|
||||
"revert": "Reverts a previous commit",
|
||||
"feat": "A new feature",
|
||||
"fix": "A bug fix"
|
||||
}"#;
|
||||
|
||||
let system_prompt = String::from(
|
||||
"You are a commit message generator that follows these rules:\
|
||||
\n1. Write in present tense\
|
||||
|
@ -115,7 +101,7 @@ impl AIPrompt {
|
|||
\nExclude anything unnecessary such as translation. Your entire response will be passed directly into git commit.\
|
||||
\n\nCode diff:\n```diff\n{}\n```",
|
||||
context,
|
||||
conventional_types,
|
||||
command.draft_config.commit_types,
|
||||
diff.diff
|
||||
);
|
||||
|
||||
|
|
|
@ -2,13 +2,17 @@ use std::io::Write;
|
|||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{error::LumenError, git_entity::GitEntity, provider::LumenProvider};
|
||||
use crate::{
|
||||
config::configuration::DraftConfig, error::LumenError, git_entity::GitEntity,
|
||||
provider::LumenProvider,
|
||||
};
|
||||
|
||||
use super::Command;
|
||||
|
||||
pub struct DraftCommand {
|
||||
pub git_entity: GitEntity,
|
||||
pub context: Option<String>,
|
||||
pub draft_config: DraftConfig,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
|
|
@ -4,9 +4,10 @@ use explain::ExplainCommand;
|
|||
use list::ListCommand;
|
||||
use std::process::Stdio;
|
||||
|
||||
use crate::config::configuration::DraftConfig;
|
||||
use crate::error::LumenError;
|
||||
use crate::git_entity::git_diff::GitDiff;
|
||||
use crate::git_entity::{GitEntity};
|
||||
use crate::git_entity::GitEntity;
|
||||
use crate::provider::LumenProvider;
|
||||
|
||||
pub mod draft;
|
||||
|
@ -20,7 +21,7 @@ pub enum CommandType {
|
|||
query: Option<String>,
|
||||
},
|
||||
List,
|
||||
Draft(Option<String>),
|
||||
Draft(Option<String>, DraftConfig),
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
@ -35,9 +36,10 @@ impl CommandType {
|
|||
Box::new(ExplainCommand { git_entity, query })
|
||||
}
|
||||
CommandType::List => Box::new(ListCommand),
|
||||
CommandType::Draft(context) => Box::new(DraftCommand {
|
||||
CommandType::Draft(context, draft_config) => Box::new(DraftCommand {
|
||||
context,
|
||||
git_entity: GitEntity::Diff(GitDiff::new(true)?),
|
||||
draft_config
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
|
74
src/config/cli.rs
Normal file
74
src/config/cli.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
use clap::{command, Parser, Subcommand, ValueEnum};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "lumen")]
|
||||
#[command(about = "AI-powered CLI tool for git commit summaries", long_about = None)]
|
||||
#[command(version)]
|
||||
pub struct Cli {
|
||||
#[arg(value_enum, short = 'p', long = "provider")]
|
||||
pub provider: Option<ProviderType>,
|
||||
|
||||
#[arg(short = 'k', long = "api-key")]
|
||||
pub api_key: Option<String>,
|
||||
|
||||
#[arg(short = 'm', long = "model")]
|
||||
pub model: Option<String>,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum, Debug)]
|
||||
pub enum ProviderType {
|
||||
Openai,
|
||||
Phind,
|
||||
Groq,
|
||||
Claude,
|
||||
Ollama,
|
||||
}
|
||||
|
||||
impl FromStr for ProviderType {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"openai" => Ok(ProviderType::Openai),
|
||||
"phind" => Ok(ProviderType::Phind),
|
||||
"groq" => Ok(ProviderType::Groq),
|
||||
"claude" => Ok(ProviderType::Claude),
|
||||
"ollama" => Ok(ProviderType::Ollama),
|
||||
_ => Err(format!("Unknown provider: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Explain the changes in a commit, or the current diff
|
||||
Explain {
|
||||
/// The commit hash to use
|
||||
#[arg(group = "target")]
|
||||
sha: Option<String>,
|
||||
|
||||
/// Explain current diff
|
||||
#[arg(long, group = "target")]
|
||||
diff: bool,
|
||||
|
||||
/// Use staged diff
|
||||
#[arg(long)]
|
||||
staged: bool,
|
||||
|
||||
/// Ask a question instead of summary
|
||||
#[arg(short, long)]
|
||||
query: Option<String>,
|
||||
},
|
||||
/// List all commits in an interactive fuzzy-finder, and summarize the changes
|
||||
List,
|
||||
/// Generate a commit message for the staged changes
|
||||
Draft {
|
||||
/// Add context to communicate intent
|
||||
#[arg(short, long)]
|
||||
context: Option<String>,
|
||||
},
|
||||
}
|
160
src/config/configuration.rs
Normal file
160
src/config/configuration.rs
Normal file
|
@ -0,0 +1,160 @@
|
|||
use crate::config::cli::ProviderType;
|
||||
use crate::error::LumenError;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
|
||||
use crate::Cli;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct LumenConfig {
|
||||
#[serde(
|
||||
default = "default_ai_provider",
|
||||
deserialize_with = "deserialize_ai_provider"
|
||||
)]
|
||||
pub ai_provider: ProviderType,
|
||||
|
||||
#[serde(default = "default_model")]
|
||||
pub model: String,
|
||||
|
||||
#[serde(default = "default_api_key")]
|
||||
pub api_key: String,
|
||||
|
||||
#[serde(default = "default_draft_config")]
|
||||
pub draft: DraftConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub explain: Option<ExplainConfig>,
|
||||
|
||||
#[serde(default)]
|
||||
pub list: Option<ListConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct DraftConfig {
|
||||
#[serde(
|
||||
default = "default_commit_types",
|
||||
deserialize_with = "deserialize_commit_types"
|
||||
)]
|
||||
pub commit_types: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct ExplainConfig {
|
||||
// Add explain-specific settings
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct ListConfig {
|
||||
// Add list-specific settings
|
||||
}
|
||||
|
||||
fn default_ai_provider() -> ProviderType {
|
||||
env::var("LUMEN_AI_PROVIDER")
|
||||
.unwrap_or_else(|_| "phind".to_string())
|
||||
.parse()
|
||||
.unwrap_or(ProviderType::Phind)
|
||||
}
|
||||
|
||||
fn deserialize_ai_provider<'de, D>(deserializer: D) -> Result<ProviderType, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
s.parse().map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
fn default_model() -> String {
|
||||
env::var("LUMEN_AI_MODEL").unwrap_or_else(|_| "".to_string())
|
||||
}
|
||||
|
||||
fn default_api_key() -> String {
|
||||
env::var("LUMEN_API_KEY").unwrap_or_else(|_| "".to_string())
|
||||
}
|
||||
|
||||
fn default_commit_types() -> String {
|
||||
r#"{
|
||||
"docs": "Documentation only changes",
|
||||
"style": "Changes that do not affect the meaning of the code",
|
||||
"refactor": "A code change that neither fixes a bug nor adds a feature",
|
||||
"perf": "A code change that improves performance",
|
||||
"test": "Adding missing tests or correcting existing tests",
|
||||
"build": "Changes that affect the build system or external dependencies",
|
||||
"ci": "Changes to our CI configuration files and scripts",
|
||||
"chore": "Other changes that don't modify src or test files",
|
||||
"revert": "Reverts a previous commit",
|
||||
"feat": "A new feature",
|
||||
"fix": "A bug fix"
|
||||
}"#
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn deserialize_commit_types<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let commit_types_map: HashMap<String, String> = HashMap::deserialize(deserializer)?;
|
||||
serde_json::to_string(&commit_types_map).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
fn default_draft_config() -> DraftConfig {
|
||||
DraftConfig {
|
||||
commit_types: default_commit_types(),
|
||||
}
|
||||
}
|
||||
|
||||
impl LumenConfig {
|
||||
pub fn Build(cli: &Cli) -> Result<Self, LumenError> {
|
||||
let config_path = "./lumen.config.json";
|
||||
let config = match LumenConfig::from_file(config_path) {
|
||||
Ok(config) => config,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let ai_provider: ProviderType = cli
|
||||
.provider
|
||||
.or_else(|| Some(config.ai_provider))
|
||||
.unwrap_or(default_ai_provider());
|
||||
|
||||
let api_key: String = cli.api_key.clone().unwrap_or(config.api_key);
|
||||
let model: String = cli.model.clone().unwrap_or(config.model);
|
||||
|
||||
Ok(LumenConfig {
|
||||
ai_provider,
|
||||
model,
|
||||
api_key,
|
||||
draft: config.draft,
|
||||
explain: None,
|
||||
list: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_file(file_path: &str) -> Result<Self, LumenError> {
|
||||
let content = match fs::read_to_string(file_path) {
|
||||
Ok(content) => content,
|
||||
// FILE DOSENT EXIST
|
||||
Err(_) => return Ok(LumenConfig::default()),
|
||||
};
|
||||
|
||||
match serde_json::from_str::<LumenConfig>(&content) {
|
||||
Ok(config) => Ok(config),
|
||||
Err(e) => {
|
||||
Err(LumenError::InvalidConfiguration(e.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LumenConfig {
|
||||
fn default() -> Self {
|
||||
LumenConfig {
|
||||
ai_provider: default_ai_provider(),
|
||||
model: default_model(),
|
||||
api_key: default_api_key(),
|
||||
draft: default_draft_config(),
|
||||
explain: None,
|
||||
list: None,
|
||||
}
|
||||
}
|
||||
}
|
4
src/config/mod.rs
Normal file
4
src/config/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod configuration;
|
||||
pub mod cli;
|
||||
|
||||
pub use configuration::LumenConfig;
|
|
@ -22,6 +22,9 @@ pub enum LumenError {
|
|||
#[error("Invalid arguments: {0}")]
|
||||
InvalidArguments(String),
|
||||
|
||||
#[error("Invalid configuration: {0}")]
|
||||
InvalidConfiguration(String),
|
||||
|
||||
#[error(transparent)]
|
||||
IoError(#[from] io::Error),
|
||||
|
||||
|
|
82
src/main.rs
82
src/main.rs
|
@ -1,76 +1,17 @@
|
|||
use clap::{command, Parser, Subcommand, ValueEnum};
|
||||
use clap::Parser;
|
||||
use config::cli::{Cli, Commands};
|
||||
use config::LumenConfig;
|
||||
use error::LumenError;
|
||||
use git_entity::{git_commit::GitCommit, git_diff::GitDiff, GitEntity};
|
||||
use std::process;
|
||||
|
||||
mod ai_prompt;
|
||||
mod command;
|
||||
mod config;
|
||||
mod error;
|
||||
mod git_entity;
|
||||
mod provider;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "lumen")]
|
||||
#[command(about = "AI-powered CLI tool for git commit summaries", long_about = None)]
|
||||
struct Cli {
|
||||
#[arg(
|
||||
value_enum,
|
||||
short = 'p',
|
||||
long = "provider",
|
||||
env("LUMEN_AI_PROVIDER"),
|
||||
default_value = "phind"
|
||||
)]
|
||||
provider: ProviderType,
|
||||
|
||||
#[arg(short = 'k', long = "api-key", env = "LUMEN_API_KEY")]
|
||||
api_key: Option<String>,
|
||||
|
||||
#[arg(short = 'm', long = "model", env = "LUMEN_AI_MODEL")]
|
||||
model: Option<String>,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum, Debug)]
|
||||
enum ProviderType {
|
||||
Openai,
|
||||
Phind,
|
||||
Groq,
|
||||
Claude,
|
||||
Ollama,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Explain the changes in a commit, or the current diff
|
||||
Explain {
|
||||
/// The commit hash to use
|
||||
#[arg(group = "target")]
|
||||
sha: Option<String>,
|
||||
|
||||
/// Explain current diff
|
||||
#[arg(long, group = "target")]
|
||||
diff: bool,
|
||||
|
||||
/// Use staged diff
|
||||
#[arg(long)]
|
||||
staged: bool,
|
||||
|
||||
/// Ask a question instead of summary
|
||||
#[arg(short, long)]
|
||||
query: Option<String>,
|
||||
},
|
||||
/// List all commits in an interactive fuzzy-finder, and summarize the changes
|
||||
List,
|
||||
/// Generate a commit message for the staged changes
|
||||
Draft {
|
||||
/// Add context to communicate intent
|
||||
#[arg(short, long)]
|
||||
context: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
if let Err(e) = run().await {
|
||||
|
@ -82,7 +23,18 @@ async fn main() {
|
|||
async fn run() -> Result<(), LumenError> {
|
||||
let cli = Cli::parse();
|
||||
let client = reqwest::Client::new();
|
||||
let provider = provider::LumenProvider::new(client, cli.provider, cli.api_key, cli.model)?;
|
||||
|
||||
let config = match LumenConfig::Build(&cli) {
|
||||
Ok(config) => config,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let provider = provider::LumenProvider::new(
|
||||
client,
|
||||
config.ai_provider,
|
||||
Some(config.api_key),
|
||||
Some(config.model),
|
||||
)?;
|
||||
let command = command::LumenCommand::new(provider);
|
||||
|
||||
match cli.command {
|
||||
|
@ -109,7 +61,7 @@ async fn run() -> Result<(), LumenError> {
|
|||
Commands::List => command.execute(command::CommandType::List).await?,
|
||||
Commands::Draft { context } => {
|
||||
command
|
||||
.execute(command::CommandType::Draft(context))
|
||||
.execute(command::CommandType::Draft(context, config.draft))
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,12 +5,12 @@ use ollama::{OllamaConfig, OllamaProvider};
|
|||
use openai::{OpenAIConfig, OpenAIProvider};
|
||||
use phind::{PhindConfig, PhindProvider};
|
||||
use thiserror::Error;
|
||||
use crate::config::cli::ProviderType;
|
||||
|
||||
use crate::{
|
||||
ai_prompt::{AIPrompt, AIPromptError},
|
||||
command::{draft::DraftCommand, explain::ExplainCommand},
|
||||
error::LumenError,
|
||||
ProviderType,
|
||||
};
|
||||
|
||||
pub mod claude;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue