Skip to content

Commit 550bd6b

Browse files
feat: Integrate viper for configuration default flags across commands and enhance logging and enrichment features
1 parent 95ab402 commit 550bd6b

File tree

6 files changed

+240
-50
lines changed

6 files changed

+240
-50
lines changed

cmd/enrich.go

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,20 @@ var enrichCmd = &cobra.Command{
2121
or by loading values from a configuration file. Optionally refetch model metadata
2222
from Hugging Face API and README before enrichment.`,
2323
RunE: func(cmd *cobra.Command, args []string) error {
24-
// Validate strategy
25-
strategy := strings.ToLower(strings.TrimSpace(enrichStrategy))
24+
// Get strategy from viper (respects config file)
25+
strategy := strings.ToLower(strings.TrimSpace(viper.GetString("enrich.strategy")))
2626
if strategy == "" {
2727
strategy = "interactive"
2828
}
2929
switch strategy {
3030
case "interactive", "file":
3131
// ok
3232
default:
33-
return fmt.Errorf("invalid --strategy %q (expected interactive|file)", enrichStrategy)
33+
return fmt.Errorf("invalid strategy %q (expected interactive|file)", strategy)
3434
}
3535

36-
// Validate log level
37-
level := strings.ToLower(strings.TrimSpace(enrichLogLevel))
36+
// Get log level from viper
37+
level := strings.ToLower(strings.TrimSpace(viper.GetString("log.level")))
3838
if level == "" {
3939
level = "standard"
4040
}
@@ -68,31 +68,35 @@ from Hugging Face API and README before enrichment.`,
6868
outPath = enrichInput // overwrite by default
6969
}
7070

71-
// Determine spec version (default to same version as input)
72-
// Note: if not specified, WriteBOM will use the BOM's existing spec version
73-
specVersion := strings.TrimSpace(enrichSpecVersion)
71+
// Get settings from viper (respects config file)
72+
specVersion := strings.TrimSpace(viper.GetString("output.specVersion"))
73+
outputFormat := viper.GetString("output.format")
74+
if outputFormat == "" {
75+
outputFormat = "auto"
76+
}
7477

7578
// Build enricher configuration
7679
cfg := enricher.Config{
7780
Strategy: strategy,
78-
ConfigFile: enrichConfigFile,
79-
RequiredOnly: enrichRequiredOnly,
80-
MinWeight: enrichMinWeight,
81-
Refetch: enrichRefetch,
82-
NoPreview: enrichNoPreview,
81+
ConfigFile: viper.GetString("enrich.configFile"),
82+
RequiredOnly: viper.GetBool("enrich.requiredOnly"),
83+
MinWeight: viper.GetFloat64("enrich.minWeight"),
84+
Refetch: viper.GetBool("enrich.refetch"),
85+
NoPreview: viper.GetBool("enrich.noPreview"),
8386
SpecVersion: specVersion,
84-
HFToken: enrichHFToken,
85-
HFBaseURL: enrichHFBaseURL,
86-
HFTimeout: enrichHFTimeout,
87+
HFToken: viper.GetString("huggingface.token"),
88+
HFBaseURL: viper.GetString("huggingface.baseURL"),
89+
HFTimeout: viper.GetInt("huggingface.timeout"),
8790
}
8891

8992
// Load config file values if using file strategy
9093
var configViper *viper.Viper
9194
if strategy == "file" {
92-
if enrichConfigFile == "" {
93-
return fmt.Errorf("--file is required when using --strategy file")
95+
configFile := cfg.ConfigFile
96+
if configFile == "" {
97+
configFile = "./config/enrichment.yaml"
9498
}
95-
configViper, err = loadEnrichmentConfig(enrichConfigFile)
99+
configViper, err = loadEnrichmentConfig(configFile)
96100
if err != nil {
97101
return fmt.Errorf("failed to load config file: %w", err)
98102
}
@@ -112,7 +116,7 @@ from Hugging Face API and README before enrichment.`,
112116
}
113117

114118
// Write output
115-
if err := bomio.WriteBOM(enriched, outPath, enrichOutputFormat, specVersion); err != nil {
119+
if err := bomio.WriteBOM(enriched, outPath, outputFormat, specVersion); err != nil {
116120
return fmt.Errorf("failed to write output: %w", err)
117121
}
118122

@@ -162,6 +166,20 @@ func init() {
162166
enrichCmd.Flags().IntVar(&enrichHFTimeout, "hf-timeout", 30, "Hugging Face API timeout in seconds (for refetch)")
163167

164168
_ = enrichCmd.MarkFlagRequired("input")
169+
170+
// Bind flags to viper for config file support
171+
viper.BindPFlag("enrich.strategy", enrichCmd.Flags().Lookup("strategy"))
172+
viper.BindPFlag("enrich.configFile", enrichCmd.Flags().Lookup("file"))
173+
viper.BindPFlag("enrich.requiredOnly", enrichCmd.Flags().Lookup("required-only"))
174+
viper.BindPFlag("enrich.minWeight", enrichCmd.Flags().Lookup("min-weight"))
175+
viper.BindPFlag("enrich.refetch", enrichCmd.Flags().Lookup("refetch"))
176+
viper.BindPFlag("enrich.noPreview", enrichCmd.Flags().Lookup("no-preview"))
177+
viper.BindPFlag("log.level", enrichCmd.Flags().Lookup("log-level"))
178+
viper.BindPFlag("huggingface.token", enrichCmd.Flags().Lookup("hf-token"))
179+
viper.BindPFlag("huggingface.baseURL", enrichCmd.Flags().Lookup("hf-base-url"))
180+
viper.BindPFlag("huggingface.timeout", enrichCmd.Flags().Lookup("hf-timeout"))
181+
viper.BindPFlag("output.format", enrichCmd.Flags().Lookup("output-format"))
182+
viper.BindPFlag("output.specVersion", enrichCmd.Flags().Lookup("spec"))
165183
}
166184

167185
// loadEnrichmentConfig loads enrichment values from a YAML config file

cmd/generate.go

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
cdx "github.com/CycloneDX/cyclonedx-go"
1111
"github.com/spf13/cobra"
12+
"github.com/spf13/viper"
1213

1314
"github.com/idlab-discover/AIBoMGen-cli/internal/builder"
1415
"github.com/idlab-discover/AIBoMGen-cli/internal/fetcher"
@@ -50,20 +51,20 @@ var generateCmd = &cobra.Command{
5051
return err
5152
}
5253

53-
// Resolve effective log level.
54-
level := strings.ToLower(strings.TrimSpace(generateLogLevel))
54+
// Resolve effective log level (from config, env, or flag).
55+
level := strings.ToLower(strings.TrimSpace(viper.GetString("log.level")))
5556
if level == "" {
5657
level = "standard"
5758
}
5859
switch level {
5960
case "quiet", "standard", "debug":
6061
// ok
6162
default:
62-
return fmt.Errorf("invalid --log-level %q (expected quiet|standard|debug)", generateLogLevel)
63+
return fmt.Errorf("invalid --log-level %q (expected quiet|standard|debug)", level)
6364
}
6465

65-
// Resolve effective HF mode.
66-
mode := strings.ToLower(strings.TrimSpace(hfMode))
66+
// Resolve effective HF mode (from config, env, or flag).
67+
mode := strings.ToLower(strings.TrimSpace(viper.GetString("huggingface.mode")))
6768
if mode == "" {
6869
mode = "online"
6970
}
@@ -73,10 +74,19 @@ var generateCmd = &cobra.Command{
7374
default:
7475
return fmt.Errorf("invalid --hf-mode %q (expected online|dummy)", hfMode)
7576
}
77+
// Get format from viper (respects config file)
78+
outputFormat := viper.GetString("output.format")
79+
if outputFormat == "" {
80+
outputFormat = "auto"
81+
}
82+
83+
// Get spec version from viper
84+
specVersion := viper.GetString("output.specVersion")
85+
7686
// Fail fast on explicit format/extension mismatch before scanning
77-
if generateOutput != "" && generateOutputFormat != "" && generateOutputFormat != "auto" {
87+
if generateOutput != "" && outputFormat != "" && outputFormat != "auto" {
7888
ext := filepath.Ext(generateOutput)
79-
if generateOutputFormat == "xml" && ext == ".json" {
89+
if outputFormat == "xml" && ext == ".json" {
8090
return fmt.Errorf("output path extension %q does not match format %q", ext, generateOutputFormat)
8191
}
8292
if generateOutputFormat == "json" && ext == ".xml" {
@@ -102,10 +112,13 @@ var generateCmd = &cobra.Command{
102112
return err
103113
}
104114

105-
if hfTimeoutSec <= 0 {
106-
hfTimeoutSec = 10
115+
// Get HF settings from viper
116+
hfToken := viper.GetString("huggingface.token")
117+
hfTimeout := viper.GetInt("huggingface.timeout")
118+
if hfTimeout <= 0 {
119+
hfTimeout = 10
107120
}
108-
timeout := time.Duration(hfTimeoutSec) * time.Second
121+
timeout := time.Duration(hfTimeout) * time.Second
109122

110123
var discoveredBOMs []generator.DiscoveredBOM
111124
if mode == "dummy" {
@@ -140,7 +153,7 @@ var generateCmd = &cobra.Command{
140153
}
141154
}
142155

143-
fmtChosen := generateOutputFormat
156+
fmtChosen := outputFormat
144157
if fmtChosen == "auto" || fmtChosen == "" {
145158
ext := filepath.Ext(output)
146159
if ext == ".xml" {
@@ -182,7 +195,7 @@ var generateCmd = &cobra.Command{
182195
fileName := fmt.Sprintf("%s_aibom%s", sanitized, fileExt)
183196
dest := filepath.Join(outputDir, fileName)
184197

185-
if err := bomio.WriteBOM(d.BOM, dest, fmtChosen, generateSpecVersion); err != nil {
198+
if err := bomio.WriteBOM(d.BOM, dest, fmtChosen, specVersion); err != nil {
186199
return err
187200
}
188201
written = append(written, dest)
@@ -209,6 +222,14 @@ func init() {
209222
generateCmd.Flags().StringVar(&hfToken, "hf-token", "", "Hugging Face access token (string)")
210223
generateCmd.Flags().BoolVar(&enrich, "enrich", false, "Prompt for missing fields and compute completeness")
211224
generateCmd.Flags().StringVar(&generateLogLevel, "log-level", "standard", "Log level: quiet|standard|debug (default: standard)")
225+
226+
// Bind flags to viper for config file support
227+
viper.BindPFlag("output.format", generateCmd.Flags().Lookup("format"))
228+
viper.BindPFlag("output.specVersion", generateCmd.Flags().Lookup("spec"))
229+
viper.BindPFlag("huggingface.mode", generateCmd.Flags().Lookup("hf-mode"))
230+
viper.BindPFlag("huggingface.timeout", generateCmd.Flags().Lookup("hf-timeout"))
231+
viper.BindPFlag("huggingface.token", generateCmd.Flags().Lookup("hf-token"))
232+
viper.BindPFlag("log.level", generateCmd.Flags().Lookup("log-level"))
212233
}
213234

214235
func bomMetadataComponentName(bom *cdx.BOM) string {

cmd/root.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"os"
7+
"strings"
78

89
"github.com/spf13/cobra"
910
"github.com/spf13/viper"
@@ -67,12 +68,20 @@ func initConfig() {
6768
home, err := os.UserHomeDir()
6869
cobra.CheckErr(err)
6970

70-
// Search config in home directory with name ".cobra" (without extension).
71+
// Search for config in multiple locations (in order of priority):
72+
// 1. $HOME/.aibomgen-cli.yaml
73+
// 2. ./config/config.yaml (project local)
7174
viper.AddConfigPath(home)
75+
viper.AddConfigPath("./config")
7276
viper.SetConfigType("yaml")
73-
viper.SetConfigName(".cobra")
77+
viper.SetConfigName(".aibomgen-cli") // for $HOME/.aibomgen-cli.yaml
78+
viper.SetConfigName("config") // for ./config/config.yaml
7479
}
7580

81+
// Enable environment variable support (e.g., AIBOMGEN_HUGGINGFACE_TOKEN)
82+
// Replace dots with underscores: huggingface.token -> AIBOMGEN_HUGGINGFACE_TOKEN
83+
viper.SetEnvPrefix("AIBOMGEN")
84+
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
7685
viper.AutomaticEnv()
7786

7887
err := viper.ReadInConfig()

cmd/validate.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/idlab-discover/AIBoMGen-cli/internal/metadata"
1010
"github.com/idlab-discover/AIBoMGen-cli/internal/validator"
1111
"github.com/spf13/cobra"
12+
"github.com/spf13/viper"
1213
)
1314

1415
var (
@@ -29,16 +30,16 @@ var validateCmd = &cobra.Command{
2930
return fmt.Errorf("--input is required")
3031
}
3132

32-
// Resolve effective log level.
33-
level := strings.ToLower(strings.TrimSpace(validateLogLevel))
33+
// Get log level from viper (respects config file)
34+
level := strings.ToLower(strings.TrimSpace(viper.GetString("log.level")))
3435
if level == "" {
3536
level = "standard"
3637
}
3738
switch level {
3839
case "quiet", "standard", "debug":
3940
// ok
4041
default:
41-
return fmt.Errorf("invalid --log-level %q (expected quiet|standard|debug)", validateLogLevel)
42+
return fmt.Errorf("invalid --log-level %q (expected quiet|standard|debug)", level)
4243
}
4344

4445
// Wire internal package logging based on log level.
@@ -84,4 +85,10 @@ func init() {
8485
validateCmd.Flags().StringVar(&validateLogLevel, "log-level", "standard", "Log level: quiet|standard|debug (default: standard)")
8586

8687
validateCmd.MarkFlagRequired("input")
88+
89+
// Bind flags to viper for config file support
90+
viper.BindPFlag("validate.strict", validateCmd.Flags().Lookup("strict"))
91+
viper.BindPFlag("validate.minScore", validateCmd.Flags().Lookup("min-score"))
92+
viper.BindPFlag("validate.checkModelCard", validateCmd.Flags().Lookup("check-model-card"))
93+
viper.BindPFlag("log.level", validateCmd.Flags().Lookup("log-level"))
8794
}

config/config.yaml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# AIBoMGen-cli Global Configuration
2+
# This file provides default values for common settings across all commands.
3+
# Values can be overridden by command-line flags or by using --config to specify a different file.
4+
# Default location: $HOME/.aibomgen-cli.yaml or ./config/config.yaml
5+
6+
# ============================================================================
7+
# Hugging Face API Settings
8+
# ============================================================================
9+
huggingface:
10+
# API token for authenticated requests (optional)
11+
# You can get a token from https://huggingface.co/settings/tokens
12+
token: ""
13+
14+
# Base URL for Hugging Face API (change only if using a private mirror)
15+
baseURL: "https://huggingface.co"
16+
17+
# API timeout in seconds
18+
timeout: 10
19+
20+
# Metadata fetching mode for generate command
21+
# Options: online (fetch from API), dummy (use mock data)
22+
mode: "online"
23+
24+
# ============================================================================
25+
# Output Settings
26+
# ============================================================================
27+
output:
28+
# Default output format for generated BOMs
29+
# Options: json, xml, auto
30+
format: "json"
31+
32+
# CycloneDX specification version
33+
# Options: 1.4, 1.5, 1.6 (empty = use latest)
34+
specVersion: ""
35+
36+
# Default output directory for generated BOMs
37+
directory: "./dist"
38+
39+
# ============================================================================
40+
# Logging Settings
41+
# ============================================================================
42+
log:
43+
# Log verbosity level
44+
# Options: quiet, standard, debug
45+
level: "standard"
46+
47+
# ============================================================================
48+
# Enrichment Settings
49+
# ============================================================================
50+
enrich:
51+
# Default enrichment strategy
52+
# Options: interactive (prompt for values), file (load from config)
53+
strategy: "interactive"
54+
55+
# Path to enrichment configuration file
56+
configFile: "./config/enrichment.yaml"
57+
58+
# Only enrich required fields
59+
requiredOnly: false
60+
61+
# Minimum field weight threshold (0.0 to 1.0)
62+
minWeight: 0.0
63+
64+
# Refetch metadata from Hugging Face before enrichment
65+
refetch: true
66+
67+
# Skip preview before saving changes
68+
noPreview: false
69+
70+
# ============================================================================
71+
# Validation Settings
72+
# ============================================================================
73+
validate:
74+
# Strict validation mode (enforce all required fields)
75+
strict: false
76+
77+
# Minimum completeness score (0.0 to 1.0)
78+
minScore: 0.0
79+
80+
# Check model card fields
81+
checkModelCard: true

0 commit comments

Comments
 (0)