c0d2a4e0025da763e51418085b1ff615bcb0c408

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

config: switch to go-flags option parsing

Diff

This diff is truncated to protect this page.

  1diff --git a/cmd/jp/main.go b/cmd/jp/main.go
  2index db386e59361cb5f90e80100bf80dd69deb48f47e..1b4bb2271a5ceb72e6fd205420deb0abdd20fc66 100644
  3--- a/cmd/jp/main.go
  4+++ b/cmd/jp/main.go
  5@@ -4,7 +4,6 @@ package main
  6 import (
  7 	"context"
  8 	"encoding/json"
  9-	"flag"
 10 	"fmt"
 11 	"io"
 12 	"net/http"
 13@@ -12,6 +11,7 @@ import (
 14 	"time"
 15 
 16 	"github.com/charmbracelet/bubbletea"
 17+	flags "github.com/jessevdk/go-flags"
 18 
 19 	"japanese/internal/adapters"
 20 	"japanese/internal/availability"
 21@@ -47,15 +47,37 @@ func main() {
 22 // (which enforces it) and to the UI (which shows it as the cap indicator).
 23 const recordCap = 10 * time.Second
 24 
 25+// parseOpts parses go-flags options for one subcommand. It prints help and
 26+// exits zero on -h/--help; any other parse error prints the error with help
 27+// and exits non-zero.
 28+func parseOpts[T any](name string, args []string) *T {
 29+	opts := new(T)
 30+	parser := flags.NewParser(opts, flags.HelpFlag)
 31+	_, err := parser.ParseArgs(args)
 32+	if err == nil {
 33+		return opts
 34+	}
 35+	if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp {
 36+		parser.WriteHelp(os.Stdout)
 37+		os.Exit(0)
 38+	}
 39+	fmt.Fprintf(os.Stderr, "jp %s: %v\n", name, err)
 40+	parser.WriteHelp(os.Stderr)
 41+	os.Exit(1)
 42+	return nil
 43+}
 44+
 45+type runOptions struct {
 46+	config.Config
 47+	MapPath  string `long:"map" env:"JP_MAP_PATH" default:"assets/maps/city.json" description:"path to the city map JSON"`
 48+	Personas string `long:"persona-dir" env:"JP_PERSONA_DIR" default:"assets/personas" description:"directory of persona JSON files"`
 49+}
 50+
 51 func runCmd(args []string) {
 52-	fs := flag.NewFlagSet("run", flag.ExitOnError)
 53-	var cfg config.Config
 54-	cfg.RegisterFlags(fs)
 55-	mapPath := fs.String("map", "assets/maps/city.json", "path to the city map JSON")
 56-	personasDir := fs.String("personas", "assets/personas", "directory of persona JSON files")
 57-	fs.Parse(args)
 58-
 59-	data, err := os.ReadFile(*mapPath)
 60+	opts := parseOpts[runOptions]("run", args)
 61+	cfg := opts.Config
 62+
 63+	data, err := os.ReadFile(opts.MapPath)
 64 	if err != nil {
 65 		fatalf("read map: %v", err)
 66 	}
 67@@ -63,7 +85,7 @@ func runCmd(args []string) {
 68 	if err != nil {
 69 		fatalf("load map: %v", err)
 70 	}
 71-	personas, err := persona.LoadDir(*personasDir)
 72+	personas, err := persona.LoadDir(opts.Personas)
 73 	if err != nil {
 74 		fatalf("load personas: %v", err)
 75 	}
 76@@ -141,23 +163,20 @@ func personaCmd(args []string) {
 77 	genPersonaCmd(args[1:])
 78 }
 79 
 80+type genOptions struct {
 81+	config.Config
 82+	Role string `long:"role" required:"true" description:"role to author a persona for"`
 83+}
 84+
 85 func genPersonaCmd(args []string) {
 86-	fs := flag.NewFlagSet("persona gen", flag.ExitOnError)
 87-	var cfg config.Config
 88-	cfg.RegisterFlags(fs)
 89-	role := fs.String("role", "", "Role to author a persona for (required)")
 90-	fs.Parse(args)
 91-	if *role == "" {
 92-		fmt.Fprintln(os.Stderr, "jp persona gen: --role is required")
 93-		os.Exit(1)
 94-	}
 95+	opts := parseOpts[genOptions]("persona gen", args)
 96 
 97 	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
 98 	defer cancel()
 99-	client := llm.NewClient(cfg.LLMBaseURL, nil)
100-	draft, err := persona.GenerateDraft(ctx, client, *role)
101+	client := llm.NewClient(opts.Config.LLMBaseURL, nil)
102+	draft, err := persona.GenerateDraft(ctx, client, opts.Role)
103 	if err != nil {
104-		fmt.Fprintf(os.Stderr, "persona gen: LLM service at %s unavailable: %v\n", cfg.LLMBaseURL, err)
105diff --git a/go.mod b/go.mod
106index 052652798fe4a04c7d7d05f343a84af496da3f96..85cb01ed073b6aaa1430fbd9525790285708da95 100644
107--- a/go.mod
108+++ b/go.mod
109@@ -16,6 +16,7 @@ require (
110 	github.com/charmbracelet/x/term v0.2.1 // indirect
111 	github.com/ebitengine/purego v0.11.0 // indirect
112 	github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
113+	github.com/jessevdk/go-flags v1.6.1 // indirect
114 	github.com/jfreymuth/pulse v0.1.3 // indirect
115 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
116 	github.com/mattn/go-isatty v0.0.20 // indirect
117diff --git a/go.sum b/go.sum
118index 60f01736a33de0ecd0c4d4a9a68077d053bb5ec2..84771d0a0b2a16e68b126abdd8d9686bda887fb5 100644
119--- a/go.sum
120+++ b/go.sum
121@@ -18,6 +18,8 @@ github.com/ebitengine/purego v0.11.0 h1:jhp/D+Nyv7UUW8HAcmcjt2N2rYrYi9m3SL21k0Ua
122 github.com/ebitengine/purego v0.11.0/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
123 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
124 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
125+github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
126+github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
127 github.com/jfreymuth/pulse v0.1.3 h1:bc5TdxiB8E+2INnFjFWWgyfgXtz2IyNNNCX+Wt/ZD14=
128 github.com/jfreymuth/pulse v0.1.3/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no=
129 github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
130diff --git a/internal/config/config.go b/internal/config/config.go
131index cebcaa07692c1975ed95311f7c0e38957a341713..be3225dcbf9ed198b975c6e43d26c8610045f872 100644
132--- a/internal/config/config.go
133+++ b/internal/config/config.go
134@@ -1,47 +1,15 @@
135-// Package config holds endpoint and recorder configuration for the game.
136-// Values come from flags and environment variables; flags win over env vars,
137-// which win over built-in defaults.
138+// Package config holds the service endpoint and recorder configuration shared
139+// by all jp subcommands. Values come from command-line flags, environment
140+// variables, or built-in defaults; flags win over env vars, which win over
141+// defaults.
142 package config
143 
144-import (
145-	"flag"
146-	"os"
147-)
148-
149-const (
150-	DefaultLLMBaseURL    = "http://127.0.0.1:8081/v1"
151-	DefaultTTSBaseURL    = "http://127.0.0.1:8080/v1"
152-	DefaultSTTURL        = "http://127.0.0.1:8178/inference"
153-	DefaultSTTLanguage   = "auto"
154-	DefaultRecordCommand = "arecord"
155-
156-	EnvLLMBaseURL  = "JP_LLM_BASE_URL"
157-	EnvTTSBaseURL  = "JP_TTS_BASE_URL"
158-	EnvSTTURL      = "JP_STT_URL"
159-	EnvSTTLanguage = "JP_STT_LANGUAGE"
160-)
161-
162+// Config is the shared option group. Subcommands embed it in their own options
163+// struct and parse with github.com/jessevdk/go-flags.
164 type Config struct {
165-	LLMBaseURL    string
166-	TTSBaseURL    string
167-	STTURL        string
168-	STTLanguage   string
169-	RecordCommand string
170-}
171-
172-func envOrDefault(key, fallback string) string {
173-	if value := os.Getenv(key); value != "" {
174-		return value
175-	}
176-	return fallback
177-}
178-
179-// RegisterFlags adds the service endpoint and recorder flags to fs, seeding
180-// each default from the matching environment variable.
181-func (c *Config) RegisterFlags(fs *flag.FlagSet) {
182-	fs.StringVar(&c.LLMBaseURL, "llm-url", envOrDefault(EnvLLMBaseURL, DefaultLLMBaseURL), "OpenAI-compatible LLM base URL")
183-	fs.StringVar(&c.TTSBaseURL, "tts-url", envOrDefault(EnvTTSBaseURL, DefaultTTSBaseURL), "OpenAI-compatible TTS base URL")
184-	fs.StringVar(&c.STTURL, "stt-url", envOrDefault(EnvSTTURL, DefaultSTTURL), "Whisper inference URL")
185-	fs.StringVar(&c.STTLanguage, "stt-language", envOrDefault(EnvSTTLanguage, DefaultSTTLanguage), "Optional Whisper request language")
186-	fs.StringVar(&c.RecordCommand, "record-command", DefaultRecordCommand, "Recorder command for 16kHz mono S16_LE WAV capture")
187+	LLMBaseURL    string `long:"llm-url" env:"JP_LLM_BASE_URL" default:"http://127.0.0.1:8081/v1" description:"OpenAI-compatible LLM base URL"`
188+	TTSBaseURL    string `long:"tts-url" env:"JP_TTS_BASE_URL" default:"http://127.0.0.1:8080/v1" description:"OpenAI-compatible TTS base URL"`
189+	STTURL        string `long:"stt-url" env:"JP_STT_URL" default:"http://127.0.0.1:8178/inference" description:"Whisper inference URL"`
190+	STTLanguage   string `long:"stt-language" env:"JP_STT_LANGUAGE" default:"auto" description:"Optional Whisper request language"`
191+	RecordCommand string `long:"record-command" default:"arecord" description:"Recorder command for 16kHz mono S16_LE WAV capture"`
192 }