Compare commits

...

3 Commits

Author SHA1 Message Date
Jamison Lahman
77ce667b21 nit 2026-04-05 18:45:22 -07:00
Jamison Lahman
1e0a8afc72 INTERNAL_URL 2026-04-05 18:05:02 -07:00
Jamison Lahman
85302a1cde feat(cli): --config-file and --server-url 2026-04-05 17:32:48 -07:00
11 changed files with 151 additions and 69 deletions

View File

@@ -6,7 +6,6 @@ import (
"text/tabwriter"
"github.com/onyx-dot-app/onyx/cli/internal/api"
"github.com/onyx-dot-app/onyx/cli/internal/config"
"github.com/onyx-dot-app/onyx/cli/internal/exitcodes"
"github.com/spf13/cobra"
)
@@ -25,7 +24,7 @@ Use --json for machine-readable output.`,
onyx-cli agents --json
onyx-cli agents --json | jq '.[].name'`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.Load()
cfg := loadConfig(cmd)
if !cfg.IsConfigured() {
return exitcodes.New(exitcodes.NotConfigured, "onyx CLI is not configured\n Run: onyx-cli configure")
}

View File

@@ -11,7 +11,6 @@ import (
"syscall"
"github.com/onyx-dot-app/onyx/cli/internal/api"
"github.com/onyx-dot-app/onyx/cli/internal/config"
"github.com/onyx-dot-app/onyx/cli/internal/exitcodes"
"github.com/onyx-dot-app/onyx/cli/internal/models"
"github.com/onyx-dot-app/onyx/cli/internal/overflow"
@@ -49,7 +48,7 @@ to a temp file. Set --max-output 0 to disable truncation.`,
cat error.log | onyx-cli ask --prompt "Find the root cause"
echo "what is onyx?" | onyx-cli ask`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.Load()
cfg := loadConfig(cmd)
if !cfg.IsConfigured() {
return exitcodes.New(exitcodes.NotConfigured, "onyx CLI is not configured\n Run: onyx-cli configure")
}

View File

@@ -21,7 +21,7 @@ an interactive setup wizard will guide you through configuration.`,
Example: ` onyx-cli chat
onyx-cli`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.Load()
cfg := loadConfig(cmd)
// First-run: onboarding
if !config.ConfigExists() || !cfg.IsConfigured() {

View File

@@ -69,7 +69,7 @@ Use --dry-run to test the connection without saving the configuration.`,
return exitcodes.New(exitcodes.BadRequest, "both --server-url and --api-key are required for non-interactive setup\n Run 'onyx-cli configure' without flags for interactive setup")
}
cfg := config.Load()
cfg := loadConfig(cmd)
onboarding.Run(&cfg)
return nil
},

View File

@@ -12,7 +12,7 @@ func newExperimentsCmd() *cobra.Command {
Use: "experiments",
Short: "List experimental features and their status",
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.Load()
cfg := loadConfig(cmd)
_, _ = fmt.Fprintln(cmd.OutOrStdout(), config.ExperimentsText(cfg.Features))
return nil
},

View File

@@ -13,6 +13,20 @@ import (
"github.com/spf13/cobra"
)
// loadConfig loads the CLI config, using the --config-file persistent flag if set.
func loadConfig(cmd *cobra.Command) config.OnyxCliConfig {
cf, _ := cmd.Flags().GetString("config-file")
return config.Load(cf)
}
// effectiveConfigPath returns the config file path, respecting --config-file.
func effectiveConfigPath(cmd *cobra.Command) string {
if cf, _ := cmd.Flags().GetString("config-file"); cf != "" {
return cf
}
return config.ConfigFilePath()
}
// Version and Commit are set via ldflags at build time.
var (
Version string
@@ -29,7 +43,7 @@ func fullVersion() string {
func printVersion(cmd *cobra.Command) {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Client version: %s\n", fullVersion())
cfg := config.Load()
cfg := loadConfig(cmd)
if !cfg.IsConfigured() {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Server version: unknown (not configured)\n")
return
@@ -84,6 +98,8 @@ func Execute() error {
}
rootCmd.PersistentFlags().BoolVar(&opts.Debug, "debug", false, "run in debug mode")
rootCmd.PersistentFlags().String("config-file", "",
"Path to config file (default: "+config.ConfigFilePath()+")")
// Custom --version flag instead of Cobra's built-in (which only shows one version string)
var showVersion bool

View File

@@ -50,16 +50,14 @@ func sessionEnv(s ssh.Session, key string) string {
return ""
}
func validateAPIKey(serverURL string, apiKey string) error {
func validateAPIKey(serverCfg config.OnyxCliConfig, apiKey string) error {
trimmedKey := strings.TrimSpace(apiKey)
if len(trimmedKey) > maxAPIKeyLength {
return fmt.Errorf("API key is too long (max %d characters)", maxAPIKeyLength)
}
cfg := config.OnyxCliConfig{
ServerURL: serverURL,
APIKey: trimmedKey,
}
cfg := serverCfg
cfg.APIKey = trimmedKey
client := api.NewClient(cfg)
ctx, cancel := context.WithTimeout(context.Background(), apiKeyValidationTimeout)
defer cancel()
@@ -83,7 +81,7 @@ type authValidatedMsg struct {
type authModel struct {
input textinput.Model
serverURL string
serverCfg config.OnyxCliConfig
state authState
apiKey string // set on successful validation
errMsg string
@@ -91,7 +89,7 @@ type authModel struct {
aborted bool
}
func newAuthModel(serverURL, initialErr string) authModel {
func newAuthModel(serverCfg config.OnyxCliConfig, initialErr string) authModel {
ti := textinput.New()
ti.Prompt = " API Key: "
ti.EchoMode = textinput.EchoPassword
@@ -102,7 +100,7 @@ func newAuthModel(serverURL, initialErr string) authModel {
return authModel{
input: ti,
serverURL: serverURL,
serverCfg: serverCfg,
errMsg: initialErr,
}
}
@@ -138,9 +136,9 @@ func (m authModel) Update(msg tea.Msg) (authModel, tea.Cmd) {
}
m.state = authValidating
m.errMsg = ""
serverURL := m.serverURL
serverCfg := m.serverCfg
return m, func() tea.Msg {
return authValidatedMsg{key: key, err: validateAPIKey(serverURL, key)}
return authValidatedMsg{key: key, err: validateAPIKey(serverCfg, key)}
}
}
@@ -171,12 +169,13 @@ func (m authModel) Update(msg tea.Msg) (authModel, tea.Cmd) {
}
func (m authModel) View() string {
settingsURL := strings.TrimRight(m.serverURL, "/") + "/app/settings/accounts-access"
serverURL := m.serverCfg.ServerURL
settingsURL := strings.TrimRight(serverURL, "/") + "/app/settings/accounts-access"
var b strings.Builder
b.WriteString("\n")
b.WriteString(" \x1b[1;35mOnyx CLI\x1b[0m\n")
b.WriteString(" \x1b[90m" + m.serverURL + "\x1b[0m\n")
b.WriteString(" \x1b[90m" + serverURL + "\x1b[0m\n")
b.WriteString("\n")
b.WriteString(" Generate an API key at:\n")
b.WriteString(" \x1b[4;34m" + settingsURL + "\x1b[0m\n")
@@ -215,7 +214,7 @@ type serveModel struct {
func newServeModel(serverCfg config.OnyxCliConfig, initialErr string) serveModel {
return serveModel{
auth: newAuthModel(serverCfg.ServerURL, initialErr),
auth: newAuthModel(serverCfg, initialErr),
serverCfg: serverCfg,
}
}
@@ -238,11 +237,8 @@ func (m serveModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit
}
if m.auth.apiKey != "" {
cfg := config.OnyxCliConfig{
ServerURL: m.serverCfg.ServerURL,
APIKey: m.auth.apiKey,
DefaultAgentID: m.serverCfg.DefaultAgentID,
}
cfg := m.serverCfg
cfg.APIKey = m.auth.apiKey
m.tui = tui.NewModel(cfg)
m.authed = true
w, h := m.width, m.height
@@ -280,6 +276,8 @@ func newServeCmd() *cobra.Command {
rateLimitPerMin int
rateLimitBurst int
rateLimitCache int
serverURL string
apiServerURL string
)
cmd := &cobra.Command{
@@ -300,9 +298,18 @@ environment variable (the --host-key flag takes precedence).`,
Example: ` onyx-cli serve --port 2222
ssh localhost -p 2222
onyx-cli serve --host 0.0.0.0 --port 2222
onyx-cli serve --idle-timeout 30m --max-session-timeout 2h`,
onyx-cli serve --idle-timeout 30m --max-session-timeout 2h
onyx-cli serve --server-url https://my-onyx.example.com
onyx-cli serve --api-server-url http://api_server:8080 # bypass nginx
onyx-cli serve --config-file /etc/onyx-cli/config.json # global flag`,
RunE: func(cmd *cobra.Command, args []string) error {
serverCfg := config.Load()
serverCfg := loadConfig(cmd)
if cmd.Flags().Changed("server-url") {
serverCfg.ServerURL = serverURL
}
if cmd.Flags().Changed("api-server-url") {
serverCfg.InternalURL = apiServerURL
}
if serverCfg.ServerURL == "" {
return exitcodes.New(exitcodes.NotConfigured, "server URL is not configured\n Run: onyx-cli configure")
}
@@ -333,7 +340,7 @@ environment variable (the --host-key flag takes precedence).`,
var envErr string
if apiKey != "" {
if err := validateAPIKey(serverCfg.ServerURL, apiKey); err != nil {
if err := validateAPIKey(serverCfg, apiKey); err != nil {
envErr = fmt.Sprintf("ONYX_API_KEY from SSH environment is invalid: %s", err.Error())
apiKey = ""
}
@@ -341,11 +348,8 @@ environment variable (the --host-key flag takes precedence).`,
if apiKey != "" {
// Env key is valid — go straight to the TUI.
cfg := config.OnyxCliConfig{
ServerURL: serverCfg.ServerURL,
APIKey: apiKey,
DefaultAgentID: serverCfg.DefaultAgentID,
}
cfg := serverCfg
cfg.APIKey = apiKey
return tui.NewModel(cfg), []tea.ProgramOption{
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
@@ -446,6 +450,10 @@ environment variable (the --host-key flag takes precedence).`,
defaultServeRateLimitCacheSize,
"Maximum number of IP limiter entries tracked in memory",
)
cmd.Flags().StringVar(&serverURL, "server-url", "",
"Onyx server URL (overrides config file and $"+config.EnvServerURL+")")
cmd.Flags().StringVar(&apiServerURL, "api-server-url", "",
"API server URL for direct access, bypassing nginx (overrides $"+config.EnvAPIServerURL+")")
return cmd
}

View File

@@ -4,10 +4,10 @@ import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/onyx-dot-app/onyx/cli/internal/api"
"github.com/onyx-dot-app/onyx/cli/internal/config"
"github.com/onyx-dot-app/onyx/cli/internal/exitcodes"
"github.com/onyx-dot-app/onyx/cli/internal/version"
log "github.com/sirupsen/logrus"
@@ -23,19 +23,21 @@ is valid. Also reports the server version and warns if it is below the
minimum required.`,
Example: ` onyx-cli validate-config`,
RunE: func(cmd *cobra.Command, args []string) error {
cfgPath := effectiveConfigPath(cmd)
// Check config file
if !config.ConfigExists() {
return exitcodes.Newf(exitcodes.NotConfigured, "config file not found at %s\n Run: onyx-cli configure", config.ConfigFilePath())
if _, err := os.Stat(cfgPath); err != nil {
return exitcodes.Newf(exitcodes.NotConfigured, "config file not found at %s\n Run: onyx-cli configure", cfgPath)
}
cfg := config.Load()
cfg := loadConfig(cmd)
// Check API key
if !cfg.IsConfigured() {
return exitcodes.New(exitcodes.NotConfigured, "API key is missing\n Run: onyx-cli configure")
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Config: %s\n", config.ConfigFilePath())
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Config: %s\n", cfgPath)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Server: %s\n", cfg.ServerURL)
// Test connection

View File

@@ -16,18 +16,30 @@ import (
"github.com/onyx-dot-app/onyx/cli/internal/config"
"github.com/onyx-dot-app/onyx/cli/internal/models"
log "github.com/sirupsen/logrus"
)
// Client is the Onyx API client.
type Client struct {
baseURL string
serverURL string // root server URL (for reachability checks)
baseURL string // API base URL (includes /api when going through nginx)
apiKey string
httpClient *http.Client // default 30s timeout for quick requests
longHTTPClient *http.Client // 5min timeout for streaming/uploads
}
// NewClient creates a new API client from config.
//
// When InternalURL is set, requests go directly to the API server (no /api
// prefix needed — mirrors INTERNAL_URL in the web server). Otherwise,
// requests go through the nginx proxy at ServerURL which strips /api.
func NewClient(cfg config.OnyxCliConfig) *Client {
baseURL := apiBaseURL(cfg)
log.WithFields(log.Fields{
"server_url": cfg.ServerURL,
"internal_url": cfg.InternalURL,
"base_url": baseURL,
}).Debug("creating API client")
var transport *http.Transport
if t, ok := http.DefaultTransport.(*http.Transport); ok {
transport = t.Clone()
@@ -35,8 +47,9 @@ func NewClient(cfg config.OnyxCliConfig) *Client {
transport = &http.Transport{}
}
return &Client{
baseURL: strings.TrimRight(cfg.ServerURL, "/"),
apiKey: cfg.APIKey,
serverURL: strings.TrimRight(cfg.ServerURL, "/"),
baseURL: baseURL,
apiKey: cfg.APIKey,
httpClient: &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
@@ -48,14 +61,27 @@ func NewClient(cfg config.OnyxCliConfig) *Client {
}
}
// apiBaseURL returns the base URL for API requests. When InternalURL is set,
// it points directly at the API server. Otherwise it goes through the nginx
// proxy at ServerURL/api.
func apiBaseURL(cfg config.OnyxCliConfig) string {
if cfg.InternalURL != "" {
return strings.TrimRight(cfg.InternalURL, "/")
}
return strings.TrimRight(cfg.ServerURL, "/") + "/api"
}
// UpdateConfig replaces the client's config.
func (c *Client) UpdateConfig(cfg config.OnyxCliConfig) {
c.baseURL = strings.TrimRight(cfg.ServerURL, "/")
c.serverURL = strings.TrimRight(cfg.ServerURL, "/")
c.baseURL = apiBaseURL(cfg)
c.apiKey = cfg.APIKey
}
func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
url := c.baseURL + path
log.WithFields(log.Fields{"method": method, "url": url}).Debug("API request")
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}
@@ -87,12 +113,16 @@ func (c *Client) doJSON(ctx context.Context, method, path string, reqBody any, r
resp, err := c.httpClient.Do(req)
if err != nil {
log.WithError(err).WithField("url", req.URL.String()).Debug("API request failed")
return err
}
defer func() { _ = resp.Body.Close() }()
log.WithFields(log.Fields{"url": req.URL.String(), "status": resp.StatusCode}).Debug("API response")
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
respBody, _ := io.ReadAll(resp.Body)
log.WithFields(log.Fields{"status": resp.StatusCode, "body": string(respBody)}).Debug("API error response")
return &OnyxAPIError{StatusCode: resp.StatusCode, Detail: string(respBody)}
}
@@ -105,16 +135,26 @@ func (c *Client) doJSON(ctx context.Context, method, path string, reqBody any, r
// TestConnection checks if the server is reachable and credentials are valid.
// Returns nil on success, or an error with a descriptive message on failure.
func (c *Client) TestConnection(ctx context.Context) error {
// Step 1: Basic reachability
req, err := c.newRequest(ctx, "GET", "/", nil)
// Step 1: Basic reachability (hit the server root, not the API prefix)
reachURL := c.serverURL
if reachURL == "" {
reachURL = c.baseURL
}
log.WithFields(log.Fields{
"reach_url": reachURL,
"base_url": c.baseURL,
}).Debug("testing connection — step 1: reachability")
req, err := http.NewRequestWithContext(ctx, "GET", reachURL+"/", nil)
if err != nil {
return fmt.Errorf("cannot connect to %s: %w", c.baseURL, err)
return fmt.Errorf("cannot connect to %s: %w", reachURL, err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("cannot connect to %s — is the server running?", c.baseURL)
log.WithError(err).Debug("reachability check failed")
return fmt.Errorf("cannot connect to %s — is the server running?", reachURL)
}
log.WithField("status", resp.StatusCode).Debug("reachability check response")
_ = resp.Body.Close()
serverHeader := strings.ToLower(resp.Header.Get("Server"))
@@ -127,7 +167,8 @@ func (c *Client) TestConnection(ctx context.Context) error {
}
// Step 2: Authenticated check
req2, err := c.newRequest(ctx, "GET", "/api/me", nil)
log.WithField("url", c.baseURL+"/me").Debug("testing connection — step 2: auth check")
req2, err := c.newRequest(ctx, "GET", "/me", nil)
if err != nil {
return fmt.Errorf("server reachable but API error: %w", err)
}
@@ -167,7 +208,7 @@ func (c *Client) TestConnection(ctx context.Context) error {
// ListAgents returns visible agents.
func (c *Client) ListAgents(ctx context.Context) ([]models.AgentSummary, error) {
var raw []models.AgentSummary
if err := c.doJSON(ctx, "GET", "/api/persona", nil, &raw); err != nil {
if err := c.doJSON(ctx, "GET", "/persona", nil, &raw); err != nil {
return nil, err
}
var result []models.AgentSummary
@@ -184,7 +225,7 @@ func (c *Client) ListChatSessions(ctx context.Context) ([]models.ChatSessionDeta
var resp struct {
Sessions []models.ChatSessionDetails `json:"sessions"`
}
if err := c.doJSON(ctx, "GET", "/api/chat/get-user-chat-sessions", nil, &resp); err != nil {
if err := c.doJSON(ctx, "GET", "/chat/get-user-chat-sessions", nil, &resp); err != nil {
return nil, err
}
return resp.Sessions, nil
@@ -193,7 +234,7 @@ func (c *Client) ListChatSessions(ctx context.Context) ([]models.ChatSessionDeta
// GetChatSession returns full details for a session.
func (c *Client) GetChatSession(ctx context.Context, sessionID string) (*models.ChatSessionDetailResponse, error) {
var resp models.ChatSessionDetailResponse
if err := c.doJSON(ctx, "GET", "/api/chat/get-chat-session/"+sessionID, nil, &resp); err != nil {
if err := c.doJSON(ctx, "GET", "/chat/get-chat-session/"+sessionID, nil, &resp); err != nil {
return nil, err
}
return &resp, nil
@@ -210,7 +251,7 @@ func (c *Client) RenameChatSession(ctx context.Context, sessionID string, name *
var resp struct {
NewName string `json:"new_name"`
}
if err := c.doJSON(ctx, "PUT", "/api/chat/rename-chat-session", payload, &resp); err != nil {
if err := c.doJSON(ctx, "PUT", "/chat/rename-chat-session", payload, &resp); err != nil {
return "", err
}
return resp.NewName, nil
@@ -236,7 +277,7 @@ func (c *Client) UploadFile(ctx context.Context, filePath string) (*models.FileD
}
_ = writer.Close()
req, err := c.newRequest(ctx, "POST", "/api/user/projects/file/upload", &buf)
req, err := c.newRequest(ctx, "POST", "/user/projects/file/upload", &buf)
if err != nil {
return nil, err
}
@@ -275,7 +316,7 @@ func (c *Client) GetBackendVersion(ctx context.Context) (string, error) {
var resp struct {
BackendVersion string `json:"backend_version"`
}
if err := c.doJSON(ctx, "GET", "/api/version", nil, &resp); err != nil {
if err := c.doJSON(ctx, "GET", "/version", nil, &resp); err != nil {
return "", err
}
return resp.BackendVersion, nil
@@ -283,7 +324,7 @@ func (c *Client) GetBackendVersion(ctx context.Context) (string, error) {
// StopChatSession sends a stop signal for a streaming session (best-effort).
func (c *Client) StopChatSession(ctx context.Context, sessionID string) {
req, err := c.newRequest(ctx, "POST", "/api/chat/stop-chat-session/"+sessionID, nil)
req, err := c.newRequest(ctx, "POST", "/chat/stop-chat-session/"+sessionID, nil)
if err != nil {
return
}

View File

@@ -64,7 +64,7 @@ func (c *Client) SendMessageStream(
return
}
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/api/chat/send-chat-message", nil)
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/chat/send-chat-message", nil)
if err != nil {
ch <- models.ErrorEvent{Error: fmt.Sprintf("request error: %v", err), IsRetryable: false}
return

View File

@@ -10,6 +10,7 @@ import (
const (
EnvServerURL = "ONYX_SERVER_URL"
EnvAPIServerURL = "ONYX_API_SERVER_URL"
EnvAPIKey = "ONYX_API_KEY"
EnvAgentID = "ONYX_PERSONA_ID"
EnvSSHHostKey = "ONYX_SSH_HOST_KEY"
@@ -27,6 +28,7 @@ type Features struct {
// OnyxCliConfig holds the CLI configuration.
type OnyxCliConfig struct {
ServerURL string `json:"server_url"`
InternalURL string `json:"internal_url,omitempty"`
APIKey string `json:"api_key"`
DefaultAgentID int `json:"default_persona_id"`
Features Features `json:"features,omitempty"`
@@ -78,30 +80,47 @@ func ConfigExists() bool {
return err == nil
}
// LoadFromDisk reads config from the file only, without applying environment
// variable overrides. Use this when you need the persisted config values
// (e.g., to preserve them during a save operation).
func LoadFromDisk() OnyxCliConfig {
// LoadFromDisk reads config from the given file path without applying
// environment variable overrides. Use this when you need the persisted
// config values (e.g., to preserve them during a save operation).
// If no path is provided, the default config file path is used.
func LoadFromDisk(path ...string) OnyxCliConfig {
p := ConfigFilePath()
if len(path) > 0 && path[0] != "" {
p = path[0]
}
cfg := DefaultConfig()
data, err := os.ReadFile(ConfigFilePath())
data, err := os.ReadFile(p)
if err == nil {
if jsonErr := json.Unmarshal(data, &cfg); jsonErr != nil {
fmt.Fprintf(os.Stderr, "warning: config file %s is malformed: %v (using defaults)\n", ConfigFilePath(), jsonErr)
fmt.Fprintf(os.Stderr, "warning: config file %s is malformed: %v (using defaults)\n", p, jsonErr)
}
}
return cfg
}
// Load reads config from file and applies environment variable overrides.
func Load() OnyxCliConfig {
cfg := LoadFromDisk()
// Load reads config from the given file path and applies environment variable
// overrides. If no path is provided, the default config file path is used.
func Load(path ...string) OnyxCliConfig {
cfg := LoadFromDisk(path...)
applyEnvOverrides(&cfg)
return cfg
}
// Environment overrides
func applyEnvOverrides(cfg *OnyxCliConfig) {
if v := os.Getenv(EnvServerURL); v != "" {
cfg.ServerURL = v
}
// ONYX_API_SERVER_URL takes precedence; fall back to INTERNAL_URL
// (the env var used by the web server) for compatibility.
if v := os.Getenv(EnvAPIServerURL); v != "" {
cfg.InternalURL = v
} else if v := os.Getenv("INTERNAL_URL"); v != "" {
cfg.InternalURL = v
}
if v := os.Getenv(EnvAPIKey); v != "" {
cfg.APIKey = v
}
@@ -117,8 +136,6 @@ func Load() OnyxCliConfig {
fmt.Fprintf(os.Stderr, "warning: invalid value %q for %s (expected true/false), ignoring\n", v, EnvStreamMarkdown)
}
}
return cfg
}
// Save writes the config to disk, creating parent directories if needed.