// cmd/sless-plan/main.go // Дата: 2026-03-12 // CLI-обёртка над `terraform plan`. // Добавляет AI-анализ вывода если AI_HINT_LEVEL > 0. // Существующий terraform plan не меняется — AI только дополняет вывод. // // Использование: // sless-plan [terraform args...] // sless-plan --ai-level=3 [terraform args...] // // Env-переменные: // AI_HINT_LEVEL — уровень анализа 0–5 (0 = выключен, по умолчанию) // AI_PROVIDER — провайдер: "google" (по умолчанию) или "cloud" // GOOGLE_AI_API_KEY — API ключ Google Gemini // GOOGLE_AI_MODEL — модель (по умолчанию "gemini-2.0-flash") // CLOUD_LLM_ENDPOINT — endpoint будущего облачного LLM // CLOUD_LLM_TOKEN — токен для облачного LLM package main import ( "bytes" "context" "flag" "fmt" "io" "os" "os/exec" "strconv" "time" "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/ai" ) func main() { // Флаг --ai-level переопределяет env AI_HINT_LEVEL aiLevel := flag.Int("ai-level", -1, "уровень AI-анализа 0–5 (-1 = читать из AI_HINT_LEVEL)") flag.Parse() // Определяем уровень: флаг > env > 0 level := resolveLevel(*aiLevel) // Собираем аргументы для terraform plan (всё что после флагов sless-plan) tfArgs := append([]string{"plan"}, flag.Args()...) // Запускаем terraform plan, захватывая вывод для AI // но одновременно выводя его пользователю (tee-эффект) planOutput, exitCode := runTerraform(tfArgs) // Если уровень > 0 — запускаем AI анализ if level > 0 { runAIAnalysis(level, planOutput) } os.Exit(exitCode) } // resolveLevel определяет уровень AI анализа. // Приоритет: --ai-level flag > AI_HINT_LEVEL env > 0 func resolveLevel(flagValue int) int { if flagValue >= 0 { // Флаг явно задан if flagValue > 5 { flagValue = 5 } return flagValue } // Читаем из env if envVal := os.Getenv("AI_HINT_LEVEL"); envVal != "" { level, err := strconv.Atoi(envVal) if err == nil && level >= 0 && level <= 5 { return level } } // TF_VAR_ai_hint_level тоже поддерживаем if envVal := os.Getenv("TF_VAR_ai_hint_level"); envVal != "" { level, err := strconv.Atoi(envVal) if err == nil && level >= 0 && level <= 5 { return level } } return 0 // по умолчанию — AI выключен } // runTerraform запускает terraform с переданными аргументами. // Вывод идёт напрямую в stdout/stderr пользователя И захватывается для AI. // Возвращает текст вывода и код завершения. func runTerraform(args []string) (string, int) { cmd := exec.Command("terraform", args...) // Захватываем stdout для AI, но также пишем напрямую в os.Stdout var buf bytes.Buffer cmd.Stdout = io.MultiWriter(os.Stdout, &buf) cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin err := cmd.Run() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { return buf.String(), exitErr.ExitCode() } fmt.Fprintf(os.Stderr, "ошибка запуска terraform: %v\n", err) return buf.String(), 1 } return buf.String(), 0 } // runAIAnalysis запускает AI анализ и выводит результат. // Не прерывает работу если AI недоступен — только показывает предупреждение. func runAIAnalysis(level int, planOutput string) { cfg := ai.Config{ HintLevel: level, Provider: getEnv("AI_PROVIDER", "google"), GoogleAPIKey: os.Getenv("GOOGLE_AI_API_KEY"), GoogleModel: getEnv("GOOGLE_AI_MODEL", "gemini-2.0-flash"), CloudEndpoint: os.Getenv("CLOUD_LLM_ENDPOINT"), CloudToken: os.Getenv("CLOUD_LLM_TOKEN"), } analyzer, err := ai.NewAnalyzer(cfg) if err != nil { fmt.Fprintf(os.Stderr, "\n⚠ AI анализ недоступен: %v\n", err) return } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() result, err := analyzer.Analyze(ctx, planOutput) if err != nil { fmt.Fprintf(os.Stderr, "\n⚠ AI анализ завершился с ошибкой: %v\n", err) return } if result != "" { fmt.Print(result) } } // getEnv возвращает значение env-переменной или defaultVal если не задана. func getEnv(key, defaultVal string) string { if val := os.Getenv(key); val != "" { return val } return defaultVal }