Refactored, added comprehensive testing.
All checks were successful
Release / release (push) Successful in 3m17s

This commit is contained in:
Jay
2025-10-26 23:23:43 -04:00
parent ec32b75267
commit 1936f055e2
61 changed files with 4678 additions and 769 deletions

141
config/config.go Normal file
View File

@@ -0,0 +1,141 @@
package config
import (
"fmt"
"os"
)
const UsageText = `Usage: aicli [OPTION]...
Send prompts and files to LLM chat endpoints.
Global:
--version display version and exit
Input:
-f, --file PATH input file (repeatable)
-F, --stdin-file treat stdin as file content
-p, --prompt TEXT prompt text (repeatable)
-pf, --prompt-file PATH read prompt from file
System:
-s, --system TEXT system prompt text
-sf, --system-file PATH read system prompt from file
(error if both -s and -sf provided)
API:
-l, --protocol PROTO openai or ollama (default: openai)
-u, --url URL endpoint (default: https://api.ppq.ai/chat/completions)
-k, --key KEY API key
-kf, --key-file PATH read API key from file
Models:
-m, --model NAME primary model (default: gpt-4o-mini)
-b, --fallback NAMES comma-separated fallback list (default: gpt-4.1-mini)
Output:
-o, --output PATH write to file (mode 0644) instead of stdout
-q, --quiet suppress progress messages
-v, --verbose log debug information to stderr
Config:
-c, --config PATH YAML config file
Environment Variables:
AICLI_API_KEY API key
AICLI_API_KEY_FILE path to API key file
AICLI_PROTOCOL API protocol
AICLI_URL endpoint URL
AICLI_MODEL primary model name
AICLI_FALLBACK comma-separated fallback models
AICLI_SYSTEM system prompt text
AICLI_SYSTEM_FILE path to system prompt file
AICLI_CONFIG_FILE path to config file
AICLI_PROMPT_FILE path to prompt file
AICLI_DEFAULT_PROMPT override default prompt
Precedence Rules:
API key: --key > --key-file > AICLI_API_KEY > AICLI_API_KEY_FILE > config key_file
System: --system > --system-file > AICLI_SYSTEM > AICLI_SYSTEM_FILE > config system_file
Config file: --config > AICLI_CONFIG_FILE
All others: flags > environment > config file > defaults
Stdin Behavior:
No flags: stdin becomes the prompt
With -p/-pf: stdin appends after explicit prompts
With -F: stdin becomes first file (path: "input")
Examples:
echo "What is Rust?" | aicli
cat log.txt | aicli -F -p "Find errors in this log"
aicli -f main.go -p "Review this code"
aicli -c ~/.aicli.yaml -f src/main.go -f src/util.go -o analysis.md
aicli -p "Context:" -pf template.txt -p "Apply to finance sector"
`
func printUsage() {
fmt.Fprint(os.Stderr, UsageText)
}
// BuildConfig resolves configuration from all sources with precedence:
// flags > env > file > defaults
func BuildConfig(args []string) (ConfigData, error) {
flags, err := parseFlags(args)
if err != nil {
return ConfigData{}, fmt.Errorf("parse flags: %w", err)
}
// Validate protocol strings before merge
if flags.protocol != "" && flags.protocol != "openai" && flags.protocol != "ollama" {
return ConfigData{}, fmt.Errorf("invalid protocol: must be openai or ollama, got: %s", flags.protocol)
}
configPath := flags.config
if configPath == "" {
configPath = os.Getenv("AICLI_CONFIG_FILE")
}
env := loadEnvironment()
// Validate env protocol
if env.protocol != "" && env.protocol != "openai" && env.protocol != "ollama" {
return ConfigData{}, fmt.Errorf("invalid protocol: must be openai or ollama, got: %s", env.protocol)
}
file, err := loadConfigFile(configPath)
if err != nil {
return ConfigData{}, fmt.Errorf("load config file: %w", err)
}
// Validate file protocol
if file.protocol != "" && file.protocol != "openai" && file.protocol != "ollama" {
return ConfigData{}, fmt.Errorf("invalid protocol: must be openai or ollama, got: %s", file.protocol)
}
cfg := mergeSources(flags, env, file)
if err := validateConfig(cfg); err != nil {
return ConfigData{}, err
}
return cfg, nil
}
// IsVersionRequest checks if --version flag was passed
func IsVersionRequest(args []string) bool {
for _, arg := range args {
if arg == "--version" {
return true
}
}
return false
}
// IsHelpRequest checks if -h or --help flag was passed
func IsHelpRequest(args []string) bool {
for _, arg := range args {
if arg == "-h" || arg == "--help" {
return true
}
}
return false
}