Skip to content

Commit 77bd086

Browse files
feat: Add validate command for evidence.yaml configuration and complete command for shell completion.
1 parent d419ea3 commit 77bd086

File tree

6 files changed

+472
-47
lines changed

6 files changed

+472
-47
lines changed

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -416,21 +416,30 @@ down-force init --stdout
416416

417417
### 4.3 検証コマンド
418418

419+
**実装済み**: `cmd/validate.go`, `internal/config/config.go`
420+
419421
```bash
420-
# 設定ファイル検証(JSONスキーマ使用)
421-
down-force validate <config-file>
422+
down-force validate <config-file> # Default: strict mode
422423

423-
# 詳細検証(UA名の重複チェックなど)
424-
down-force validate <config-file> --strict
424+
down-force validate <config-file> --lax # Lenient mode (schema only)
425425
```
426426

427+
strict モードでは以下を検証:
428+
- JSON Schema validation
429+
- UA名の重複チェック
430+
- ディレクトリ名として有効なUA名チェック
431+
- 必須フィールドの存在チェック
432+
427433
### 4.4 補完コマンド
428434

435+
**実装済み**: `cmd/complete.go`
436+
429437
```bash
430438
# evidence.yamlにUA文字列を自動追加(べき等性確保)
431439
down-force complete <config-file>
432440

433-
# 出力例: user_agent_stringが自動追加される
441+
# プレビューのみ(ファイル変更なし)
442+
down-force complete <config-file> --dry-run
434443
```
435444

436445
## 5. 処理フロー

cmd/complete.go

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,93 @@
11
/*
2-
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
3-
2+
Copyright © 2025 canaria-computer
43
*/
54
package cmd
65

76
import (
87
"fmt"
8+
"os"
99

10+
"github.com/canaria-computer/down-force/internal/config"
11+
"github.com/charmbracelet/log"
1012
"github.com/spf13/cobra"
1113
)
1214

15+
var (
16+
completeDryRun bool
17+
)
18+
1319
// completeCmd represents the complete command
1420
var completeCmd = &cobra.Command{
15-
Use: "complete",
16-
Short: "A brief description of your command",
17-
Long: `A longer description that spans multiple lines and likely contains examples
18-
and usage of using your command. For example:
19-
20-
Cobra is a CLI library for Go that empowers applications.
21-
This application is a tool to generate the needed files
22-
to quickly create a Cobra application.`,
23-
Run: func(cmd *cobra.Command, args []string) {
24-
fmt.Println("complete called")
25-
},
21+
Use: "complete <config-file>",
22+
Short: "Auto-complete user agent strings in evidence.yaml",
23+
Long: `Auto-complete missing user agent strings in an evidence.yaml file.
24+
25+
When a user agent entry uses a builtin name (e.g., "Desktop_Chrome") but doesn't
26+
specify a user_agent_string, this command fills in the builtin UA string and
27+
viewport dimensions.
28+
29+
This ensures idempotency - the config file will contain the exact UA strings
30+
that will be used during evidence collection.
31+
32+
Examples:
33+
# Complete missing UA strings in place
34+
down-force complete evidence.yaml
35+
36+
# Preview changes without modifying the file
37+
down-force complete evidence.yaml --dry-run
38+
`,
39+
Args: cobra.ExactArgs(1),
40+
Run: runComplete,
2641
}
2742

2843
func init() {
29-
liteCmd.AddCommand(completeCmd)
44+
rootCmd.AddCommand(completeCmd)
45+
46+
completeCmd.Flags().BoolVar(&completeDryRun, "dry-run", false, "Preview changes without modifying the file")
47+
}
48+
49+
func runComplete(cmd *cobra.Command, args []string) {
50+
configPath := args[0]
51+
52+
// Check if config file exists
53+
if _, err := os.Stat(configPath); os.IsNotExist(err) {
54+
log.Fatalf("❌ Config file not found: %s", configPath)
55+
}
56+
57+
// Load and complete config
58+
cfg, modified, err := config.LoadAndCompleteConfig(configPath)
59+
if err != nil {
60+
log.Fatalf("❌ Failed to load config: %v", err)
61+
}
62+
63+
if !modified {
64+
log.Infof("✅ No changes needed: all user agents are already complete")
65+
return
66+
}
67+
68+
// Show what was completed
69+
fmt.Println("📝 Changes to be applied:")
70+
fmt.Println()
71+
72+
for _, ua := range cfg.UserAgents {
73+
builtin := config.GetBuiltinUserAgent(ua.Name)
74+
if builtin != nil && ua.UserAgentString == builtin.UserAgentString {
75+
fmt.Printf(" • %s\n", ua.Name)
76+
fmt.Printf(" UA: %s...\n", truncateString(ua.UserAgentString, 60))
77+
fmt.Printf(" Viewport: %dx%d\n", ua.ViewportWidth, ua.ViewportHeight)
78+
fmt.Println()
79+
}
80+
}
3081

31-
// Here you will define your flags and configuration settings.
82+
if completeDryRun {
83+
log.Info("🔍 Dry run - no changes written")
84+
return
85+
}
3286

33-
// Cobra supports Persistent Flags which will work for this command
34-
// and all subcommands, e.g.:
35-
// completeCmd.PersistentFlags().String("foo", "", "A help for foo")
87+
// Save the completed config
88+
if err := config.SaveConfig(configPath, cfg); err != nil {
89+
log.Fatalf("❌ Failed to save config: %v", err)
90+
}
3691

37-
// Cobra supports local flags which will only run when this command
38-
// is called directly, e.g.:
39-
// completeCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
92+
log.Infof("✅ Updated %s with complete user agent strings", configPath)
4093
}

cmd/validate.go

Lines changed: 111 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,130 @@
11
/*
2-
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
3-
2+
Copyright © 2025 canaria-computer
43
*/
54
package cmd
65

76
import (
87
"fmt"
8+
"os"
9+
"path/filepath"
910

11+
"github.com/canaria-computer/down-force/internal/config"
12+
"github.com/charmbracelet/log"
1013
"github.com/spf13/cobra"
1114
)
1215

16+
var (
17+
validateLax bool
18+
)
19+
1320
// validateCmd represents the validate command
1421
var validateCmd = &cobra.Command{
15-
Use: "validate",
16-
Short: "A brief description of your command",
17-
Long: `A longer description that spans multiple lines and likely contains examples
18-
and usage of using your command. For example:
19-
20-
Cobra is a CLI library for Go that empowers applications.
21-
This application is a tool to generate the needed files
22-
to quickly create a Cobra application.`,
23-
Run: func(cmd *cobra.Command, args []string) {
24-
fmt.Println("validate called")
25-
},
22+
Use: "validate <config-file>",
23+
Short: "Validate an evidence.yaml configuration file",
24+
Long: `Validate an evidence.yaml configuration file against the JSON schema.
25+
26+
By default, strict validation is performed which includes:
27+
- JSON Schema validation
28+
- Duplicate user agent name detection
29+
- Directory name validity check for UA names
30+
- Required field checks
31+
32+
Use --lax for lenient validation (schema only).
33+
34+
Examples:
35+
# Strict validation (default)
36+
down-force validate evidence.yaml
37+
38+
# Lenient validation (schema only)
39+
down-force validate evidence.yaml --lax
40+
`,
41+
Args: cobra.ExactArgs(1),
42+
Run: runValidate,
2643
}
2744

2845
func init() {
29-
liteCmd.AddCommand(validateCmd)
46+
rootCmd.AddCommand(validateCmd)
47+
48+
validateCmd.Flags().BoolVar(&validateLax, "lax", false, "Lenient validation (schema only, skip strict checks)")
49+
}
50+
51+
func runValidate(cmd *cobra.Command, args []string) {
52+
configPath := args[0]
53+
54+
// Check if config file exists
55+
if _, err := os.Stat(configPath); os.IsNotExist(err) {
56+
log.Fatalf("❌ Config file not found: %s", configPath)
57+
}
58+
59+
// Find schema file
60+
schemaPath, err := findSchemaPath()
61+
if err != nil {
62+
log.Fatalf("❌ Failed to find schema: %v", err)
63+
}
64+
65+
// Validate
66+
strict := !validateLax
67+
result, err := config.ValidateConfigFromFile(configPath, schemaPath, strict)
68+
if err != nil {
69+
log.Fatalf("❌ Validation failed: %v", err)
70+
}
71+
72+
// Report results
73+
if result.Valid {
74+
mode := "strict"
75+
if validateLax {
76+
mode = "lax"
77+
}
78+
log.Infof("✅ Validation passed (%s mode): %s", mode, configPath)
79+
} else {
80+
log.Errorf("❌ Validation failed: %d error(s) found", len(result.Errors))
81+
fmt.Println()
82+
83+
for i, verr := range result.Errors {
84+
path := verr.Path
85+
if path == "" {
86+
path = "(root)"
87+
}
88+
fmt.Printf(" %d. %s\n", i+1, verr.Message)
89+
fmt.Printf(" Path: %s\n", path)
90+
if verr.Value != nil {
91+
fmt.Printf(" Value: %v\n", verr.Value)
92+
}
93+
fmt.Println()
94+
}
95+
96+
os.Exit(1)
97+
}
98+
}
99+
100+
// findSchemaPath locates the JSON schema file
101+
func findSchemaPath() (string, error) {
102+
// Try relative paths first
103+
candidates := []string{
104+
"schema/evidence.schema.json",
105+
"../schema/evidence.schema.json",
106+
"../../schema/evidence.schema.json",
107+
}
30108

31-
// Here you will define your flags and configuration settings.
109+
// Get executable path and try relative to it
110+
execPath, err := os.Executable()
111+
if err == nil {
112+
execDir := filepath.Dir(execPath)
113+
candidates = append(candidates,
114+
filepath.Join(execDir, "schema", "evidence.schema.json"),
115+
filepath.Join(execDir, "..", "schema", "evidence.schema.json"),
116+
)
117+
}
32118

33-
// Cobra supports Persistent Flags which will work for this command
34-
// and all subcommands, e.g.:
35-
// validateCmd.PersistentFlags().String("foo", "", "A help for foo")
119+
for _, path := range candidates {
120+
absPath, err := filepath.Abs(path)
121+
if err != nil {
122+
continue
123+
}
124+
if _, err := os.Stat(absPath); err == nil {
125+
return absPath, nil
126+
}
127+
}
36128

37-
// Cobra supports local flags which will only run when this command
38-
// is called directly, e.g.:
39-
// validateCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
129+
return "", fmt.Errorf("schema file not found, searched: %v", candidates)
40130
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ toolchain go1.24.11
66

77
require (
88
github.com/charmbracelet/huh v0.8.0
9+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
910
golang.org/x/net v0.47.0
11+
gopkg.in/yaml.v3 v3.0.1
1012
)
1113

1214
require (

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
7777
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
7878
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
7979
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
80+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
81+
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
8082
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
8183
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
8284
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
@@ -116,6 +118,7 @@ golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
116118
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
117119
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
118120
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
121+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
119122
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
120123
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
121124
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)