159 lines
4.1 KiB
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())
|
|
}
|