Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ func main() {
Aliases: []string{"v"},
Usage: "print the version",
}
configFile := os.ExpandEnv(fmt.Sprintf("%s/%s/%s", "$HOME", model.COMMAND_BASE_STORAGE_FOLDER, "config.toml"))
configService := model.NewConfigService(configFile)
configDir := os.ExpandEnv(fmt.Sprintf("%s/%s", "$HOME", model.COMMAND_BASE_STORAGE_FOLDER))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The path construction is not platform-independent. It uses a hardcoded path separator / and relies on shell expansion of $HOME. It's better to use os.UserHomeDir() and filepath.Join for creating paths to ensure cross-platform compatibility. The model package already provides a helper function GetBaseStoragePath for this.

Suggested change
configDir := os.ExpandEnv(fmt.Sprintf("%s/%s", "$HOME", model.COMMAND_BASE_STORAGE_FOLDER))
configDir := model.GetBaseStoragePath()
References
  1. For platform-independent paths, use filepath.Join to combine segments and os.UserHomeDir() to get the home directory, rather than hardcoding path separators or environment variables like $HOME.

configService := model.NewConfigService(configDir)

uptraceOptions := []uptrace.Option{
uptrace.WithDSN(uptraceDsn),
Expand Down Expand Up @@ -107,6 +107,7 @@ func main() {
commands.DoctorCommand,
commands.QueryCommand,
commands.CCCommand,
commands.SchemaCommand,
}
err = app.Run(os.Args)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions cmd/daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ func main() {
slog.SetDefault(l)

ctx := context.Background()
configFile := os.ExpandEnv(fmt.Sprintf("%s/%s/%s", "$HOME", model.COMMAND_BASE_STORAGE_FOLDER, "config.toml"))
daemonConfigService := model.NewConfigService(configFile)
configDir := os.ExpandEnv(fmt.Sprintf("%s/%s", "$HOME", model.COMMAND_BASE_STORAGE_FOLDER))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The path construction is not platform-independent. It uses a hardcoded path separator / and relies on shell expansion of $HOME. It's better to use os.UserHomeDir() and filepath.Join for creating paths to ensure cross-platform compatibility. The model package already provides a helper function GetBaseStoragePath for this.

Suggested change
configDir := os.ExpandEnv(fmt.Sprintf("%s/%s", "$HOME", model.COMMAND_BASE_STORAGE_FOLDER))
configDir := model.GetBaseStoragePath()
References
  1. For platform-independent paths, use filepath.Join to combine segments and os.UserHomeDir() to get the home directory, rather than hardcoding path separators or environment variables like $HOME.

daemonConfigService := model.NewConfigService(configDir)
cfg, err := daemonConfigService.ReadConfigFile(ctx)
if err != nil {
slog.Error("Failed to get daemon config", slog.Any("err", err))
Expand Down
41 changes: 37 additions & 4 deletions commands/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ package commands

import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"time"

"github.com/briandowns/spinner"
"github.com/gookit/color"
"github.com/invopop/jsonschema"
"github.com/malamtime/cli/model"
"github.com/pelletier/go-toml/v2"
"github.com/pkg/browser"
"github.com/urfave/cli/v2"
"go.opentelemetry.io/otel/trace"
"gopkg.in/yaml.v3"
)

var AuthCommand *cli.Command = &cli.Command{
Expand Down Expand Up @@ -42,13 +44,22 @@ func commandAuth(c *cli.Context) error {
}
SetupLogger(configDir)

// Generate JSON schema for IDE autocompletion
schemaFile := configDir + "/config.schema.json"
if err := generateSchemaFile(schemaFile); err != nil {
slog.Warn("Failed to generate schema file", slog.Any("err", err))
}

var config model.ShellTimeConfig
configFile := configDir + "/config.toml"
configFile := configDir + "/config.yaml"
if _, err := os.Stat(configFile); os.IsNotExist(err) {
content, err := toml.Marshal(model.DefaultConfig)
content, err := yaml.Marshal(model.DefaultConfig)
if err != nil {
return fmt.Errorf("failed to marshal default config: %w", err)
}
// Prepend $schema for IDE autocompletion support
schemaHeader := "# yaml-language-server: $schema=" + schemaFile + "\n"
content = append([]byte(schemaHeader), content...)
err = os.WriteFile(configFile, content, 0644)
if err != nil {
return fmt.Errorf("failed to create config file: %w", err)
Expand All @@ -73,7 +84,7 @@ func commandAuth(c *cli.Context) error {
}

config.Token = newToken
content, err := toml.Marshal(config)
content, err := yaml.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
Expand Down Expand Up @@ -129,3 +140,25 @@ func ApplyTokenByHandshake(_ctx context.Context, config model.ShellTimeConfig) (
time.Sleep(2 * time.Second)
}
}

// generateSchemaFile generates the JSON schema file for config autocompletion
func generateSchemaFile(path string) error {
reflector := &jsonschema.Reflector{
AllowAdditionalProperties: false,
}

schema := reflector.Reflect(&model.ShellTimeConfig{})
schema.Title = "ShellTime Configuration"
schema.Description = "Configuration schema for shelltime CLI. Supports both YAML (.yaml, .yml) and TOML (.toml) formats."

schemaJSON, err := json.MarshalIndent(schema, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal schema: %w", err)
}

if err := os.WriteFile(path, schemaJSON, 0644); err != nil {
return fmt.Errorf("failed to write schema file: %w", err)
}

return nil
}
49 changes: 49 additions & 0 deletions commands/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package commands

import (
"encoding/json"
"fmt"
"os"

"github.com/invopop/jsonschema"
"github.com/malamtime/cli/model"
"github.com/urfave/cli/v2"
)

var SchemaCommand = &cli.Command{
Name: "schema",
Usage: "Generate JSON schema for config file (for IDE autocompletion)",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Output file path (default: stdout)",
},
},
Action: func(c *cli.Context) error {
reflector := &jsonschema.Reflector{
AllowAdditionalProperties: false,
}

schema := reflector.Reflect(&model.ShellTimeConfig{})
schema.Title = "ShellTime Configuration"
schema.Description = "Configuration schema for shelltime CLI. Supports both YAML (.yaml, .yml) and TOML (.toml) formats."

schemaJSON, err := json.MarshalIndent(schema, "", " ")
if err != nil {
return fmt.Errorf("failed to generate schema: %w", err)
}

outputPath := c.String("output")
if outputPath != "" {
if err := os.WriteFile(outputPath, schemaJSON, 0644); err != nil {
return fmt.Errorf("failed to write schema file: %w", err)
}
fmt.Printf("Schema written to %s\n", outputPath)
} else {
fmt.Println(string(schemaJSON))
}

return nil
},
}
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/go-git/go-git/v5 v5.16.4
github.com/google/uuid v1.6.0
github.com/gookit/color v1.6.0
github.com/invopop/jsonschema v0.13.0
github.com/lithammer/shortuuid/v3 v3.0.7
github.com/olekukonko/tablewriter v1.1.2
github.com/pelletier/go-toml/v2 v2.2.4
Expand All @@ -24,12 +25,15 @@ require (
go.opentelemetry.io/otel/trace v1.39.0
go.opentelemetry.io/proto/otlp v1.9.0
google.golang.org/grpc v1.77.0
gopkg.in/yaml.v3 v3.0.1
)

require (
atomicgo.dev/cursor v0.2.0 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.1.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/displaywidth v0.6.1 // indirect
Expand All @@ -50,6 +54,7 @@ require (
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
Expand All @@ -61,6 +66,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
Expand All @@ -83,5 +89,4 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQA
github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=
github.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w=
github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
Expand Down Expand Up @@ -88,8 +92,11 @@ github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
Expand All @@ -108,6 +115,8 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
Expand Down Expand Up @@ -168,6 +177,8 @@ github.com/uptrace/uptrace-go v1.39.0 h1:MszuE3eX/z86xzYywN2JBtYcmsS4ofdo1VMDhRv
github.com/uptrace/uptrace-go v1.39.0/go.mod h1:FquipEqgTMXPbhdhenjbiLHG1R5WYdxVH6zgwHeMzzA=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
Expand Down
111 changes: 91 additions & 20 deletions model/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,76 @@ import (
"sync"

"github.com/pelletier/go-toml/v2"
"gopkg.in/yaml.v3"
)

// configFormat represents the format of a config file
type configFormat string

const (
formatTOML configFormat = "toml"
formatYAML configFormat = "yaml"
)

// detectFormat returns the format based on file extension
func detectFormat(filename string) configFormat {
ext := strings.ToLower(filepath.Ext(filename))
switch ext {
case ".yaml", ".yml":
return formatYAML
default:
return formatTOML
}
}

// unmarshalConfig unmarshals config data based on format
func unmarshalConfig(data []byte, format configFormat, config *ShellTimeConfig) error {
switch format {
case formatYAML:
return yaml.Unmarshal(data, config)
default:
return toml.Unmarshal(data, config)
}
}

// configFiles represents discovered config files
type configFiles struct {
baseFile string // config.yaml, config.yml, or config.toml
localFile string // config.local.yaml, config.local.yml, or config.local.toml
baseFormat configFormat
localFormat configFormat
}

// findConfigFiles discovers config files in priority order
// Priority: config.local.yaml > config.local.yml > config.yaml > config.yml > config.local.toml > config.toml
Comment on lines +52 to +53

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment describing the priority order is misleading and doesn't accurately reflect what the function does. The function finds the highest-priority base file and local file separately, rather than using a single flat priority list. The comment should be updated to clarify this.

Suggested change
// findConfigFiles discovers config files in priority order
// Priority: config.local.yaml > config.local.yml > config.yaml > config.yml > config.local.toml > config.toml
// findConfigFiles discovers config files based on a priority order.
// It finds the highest-priority base file and local file separately.

func findConfigFiles(configDir string) configFiles {
result := configFiles{}

// Check for base config files (YAML first, then TOML)
baseFiles := []string{"config.yaml", "config.yml", "config.toml"}
for _, name := range baseFiles {
path := filepath.Join(configDir, name)
if _, err := os.Stat(path); err == nil {
result.baseFile = path
result.baseFormat = detectFormat(name)
break
}
}

// Check for local config files (YAML first, then TOML)
localFiles := []string{"config.local.yaml", "config.local.yml", "config.local.toml"}
for _, name := range localFiles {
path := filepath.Join(configDir, name)
if _, err := os.Stat(path); err == nil {
result.localFile = path
result.localFormat = detectFormat(name)
break
}
}
Comment on lines +57 to +77

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for finding base and local config files is duplicated. Consider extracting this logic into a helper function to improve code clarity and reduce duplication. A single function could handle iterating through a list of potential filenames and returning the first one that exists.


return result
}

var UserShellTimeConfig ShellTimeConfig

type ConfigService interface {
Expand All @@ -33,14 +101,17 @@ func WithSkipCache() ReadConfigOption {
}

type configService struct {
configFilePath string
cachedConfig *ShellTimeConfig
mu sync.RWMutex
configDir string
cachedConfig *ShellTimeConfig
mu sync.RWMutex
}

func NewConfigService(configFilePath string) ConfigService {
// NewConfigService creates a new ConfigService that reads config files from the given directory.
// It supports both YAML (.yaml, .yml) and TOML (.toml) formats with priority:
// config.local.yaml > config.local.yml > config.yaml > config.yml > config.local.toml > config.toml
Comment on lines +110 to +111

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This priority list in the comment is misleading. The service supports a base configuration file and a local override file, which can have different formats. The priority is applied separately for base files and for local files. It would be clearer to document these priorities separately.

Suggested change
// It supports both YAML (.yaml, .yml) and TOML (.toml) formats with priority:
// config.local.yaml > config.local.yml > config.yaml > config.yml > config.local.toml > config.toml
// It supports both YAML (.yaml, .yml) and TOML (.toml) formats.
// Priorities are: Base(config.yaml > .yml > .toml), Local(config.local.yaml > .yml > .toml)

func NewConfigService(configDir string) ConfigService {
return &configService{
configFilePath: configFilePath,
configDir: configDir,
}
}

Expand Down Expand Up @@ -121,32 +192,32 @@ func (cs *configService) ReadConfigFile(ctx context.Context, opts ...ReadConfigO
cs.mu.RUnlock()
}

configFile := cs.configFilePath
existingConfig, err := os.ReadFile(configFile)
// Discover config files with priority
files := findConfigFiles(cs.configDir)

// Read base config file
if files.baseFile == "" {
err = fmt.Errorf("no config file found in %s", cs.configDir)
return
}

existingConfig, err := os.ReadFile(files.baseFile)
if err != nil {
err = fmt.Errorf("failed to read config file: %w", err)
return
}

err = toml.Unmarshal(existingConfig, &config)
err = unmarshalConfig(existingConfig, files.baseFormat, &config)
if err != nil {
err = fmt.Errorf("failed to parse config file: %w", err)
return
}

// Check for local config file and merge if exists
// Extract the file extension and construct local config filename
ext := filepath.Ext(configFile)
if ext != "" {
// Get the base name without extension
baseName := strings.TrimSuffix(configFile, ext)
// Construct local config filename: baseName + ".local" + ext
localConfigFile := baseName + ".local" + ext
if localConfig, localErr := os.ReadFile(localConfigFile); localErr == nil {
// Parse local config and merge with base config
// Read and merge local config if exists
if files.localFile != "" {
if localConfig, localErr := os.ReadFile(files.localFile); localErr == nil {
var localSettings ShellTimeConfig
if unmarshalErr := toml.Unmarshal(localConfig, &localSettings); unmarshalErr == nil {
// Merge local settings into base config
if unmarshalErr := unmarshalConfig(localConfig, files.localFormat, &localSettings); unmarshalErr == nil {
mergeConfig(&config, &localSettings)
}
Comment on lines +220 to 222

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Errors from unmarshalling the local config file are silently ignored. If a user has a malformed local config, they would likely expect an error message instead of their settings being silently disregarded. It's better to propagate this error.

if unmarshalErr := unmarshalConfig(localConfig, files.localFormat, &localSettings); unmarshalErr != nil {
				err = fmt.Errorf("failed to parse local config file %s: %w", files.localFile, unmarshalErr)
				return
			}
			mergeConfig(&config, &localSettings)

}
Expand Down
Loading
Loading