Cybersecurity-Projects/PROJECTS/intermediate/security-news-scraper/internal/setup/wizard.go

159 lines
4.1 KiB
Go

// ©AngelaMos | 2026
// wizard.go
package setup
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"golang.org/x/term"
)
const (
ProviderClaude = "anthropic"
ProviderOpenAI = "openai"
ProviderGemini = "gemini"
ProviderQwen = "qwen"
envOpenAIKey = "OPENAI_API_KEY"
envGeminiKey = "GEMINI_API_KEY"
envAnthropicKey = "ANTHROPIC_API_KEY"
envNVDKey = "NVD_API_KEY"
composeQwenURL = "http://localhost:39847/v1"
nativeQwenURL = "http://localhost:11434/v1"
detectTimeout = 3 * time.Second
ollamaInstall = "https://ollama.com/download"
)
type apiChoice struct {
provider string
envKey string
label string
}
var apiProviders = map[string]apiChoice{
"1": {ProviderClaude, envAnthropicKey, "Claude (Anthropic)"},
"2": {ProviderOpenAI, envOpenAIKey, "OpenAI"},
"3": {ProviderGemini, envGeminiKey, "Gemini (Google)"},
}
func Run(in io.Reader, out io.Writer) error {
fmt.Fprintln(out, "nadezhda AI setup")
fmt.Fprintln(out, " 1) Claude (Anthropic) 2) OpenAI 3) Gemini 4) Ollama (local qwen, free, no key)")
choice := prompt(in, out, "choose a provider [1-4]: ")
switch choice {
case "1", "2", "3":
return setupAPIProvider(in, out, apiProviders[choice])
case "4":
return setupOllama(out)
default:
return fmt.Errorf("setup: invalid choice %q (want 1-4)", choice)
}
}
func setupAPIProvider(in io.Reader, out io.Writer, c apiChoice) error {
fmt.Fprintf(out, "\nConfiguring %s.\n", c.label)
key := strings.TrimSpace(readSecret(in, out, fmt.Sprintf("paste your %s API key: ", c.label)))
if key == "" {
return fmt.Errorf("setup: no API key entered")
}
updates := map[string]string{
EnvProvider: c.provider,
c.envKey: key,
}
if nvd := strings.TrimSpace(readSecret(in, out, "optional NVD token for the CVE booster (enter to skip): ")); nvd != "" {
updates[envNVDKey] = nvd
}
if err := Save(updates); err != nil {
return err
}
path, _ := CredentialsPath()
fmt.Fprintf(out, "\nSaved to %s (0600). %s is ready — run: nadezhda ideate\n", path, c.label)
return nil
}
func setupOllama(out io.Writer) error {
fmt.Fprintln(out, "\nConfiguring Ollama (local qwen2.5, no API key).")
client := &http.Client{Timeout: detectTimeout}
if base := DetectOllama(client); base != "" {
fmt.Fprintf(out, "Found a running Ollama at %s — using it.\n", base)
return saveOllama(out, base+"/v1")
}
fmt.Fprintln(out, "No running Ollama found.")
switch {
case HasBinary("docker"):
dir, _ := os.Getwd()
path, err := WriteCompose(dir)
if err != nil {
return err
}
fmt.Fprintf(out, "Wrote %s. Start it with:\n docker compose -f %s up -d\n", path, path)
fmt.Fprintln(out, "(first run pulls qwen2.5:7b, a few minutes), then re-run `nadezhda ai` and pick 4.")
return saveOllama(out, composeQwenURL)
case HasBinary("ollama"):
fmt.Fprintln(out, "Ollama is installed but not running. Start it and pull the model:")
fmt.Fprintln(out, " ollama serve & then: ollama pull qwen2.5:7b")
return saveOllama(out, nativeQwenURL)
default:
fmt.Fprintf(out, "Install Ollama (%s) or Docker, then re-run `nadezhda ai`.\n", ollamaInstall)
return saveOllama(out, composeQwenURL)
}
}
func saveOllama(out io.Writer, baseURL string) error {
if err := Save(map[string]string{EnvProvider: ProviderQwen, EnvQwenURL: baseURL}); err != nil {
return err
}
path, _ := CredentialsPath()
fmt.Fprintf(out, "Saved qwen (%s) to %s. Run: nadezhda ideate\n", baseURL, path)
return nil
}
func prompt(in io.Reader, out io.Writer, label string) string {
fmt.Fprint(out, label)
return readLine(in)
}
func readSecret(in io.Reader, out io.Writer, label string) string {
fmt.Fprint(out, label)
if f, ok := in.(*os.File); ok && term.IsTerminal(int(f.Fd())) {
b, err := term.ReadPassword(int(f.Fd()))
fmt.Fprintln(out)
if err != nil {
return ""
}
return string(b)
}
return readLine(in)
}
func readLine(in io.Reader) string {
var b strings.Builder
buf := make([]byte, 1)
for {
n, err := in.Read(buf)
if n > 0 {
if buf[0] == '\n' {
break
}
if buf[0] != '\r' {
b.WriteByte(buf[0])
}
}
if err != nil {
break
}
}
return strings.TrimSpace(b.String())
}