Skip to content
Open
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
7 changes: 7 additions & 0 deletions cmd/vale/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ type command struct {

// commands are the available CLI commands.
var commands = map[string]command{
"init": {
Run: initCmd,
Summary: "Initialize a new .vale.ini configuration file.",
Usage: "init [path]",
Detail: "Generates a default .vale.ini configuration file with baseline\n" +
"settings and creates the styles directory if no configuration exists.",
},
"ls-config": {
Run: printConfig,
Summary: "Print the current configuration to stdout.",
Expand Down
250 changes: 250 additions & 0 deletions cmd/vale/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
package main

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/errata-ai/ini"
"github.com/vale-cli/vale/v3/internal/core"
"github.com/vale-cli/vale/v3/internal/system"
)

func TestInitCmd_CreatesValidConfig(t *testing.T) {
tmpDir := t.TempDir()
cfgPath := filepath.Join(tmpDir, ".vale.ini")

if system.FileExists(cfgPath) {
t.Fatalf("expected config to not exist initially: %s", cfgPath)
}

err := initCmd([]string{cfgPath}, &core.CLIFlags{})
if err != nil {
t.Fatalf("initCmd failed: %v", err)
}

if !system.FileExists(cfgPath) {
t.Fatalf("expected config file to be created: %s", cfgPath)
}

stylesDir := filepath.Join(tmpDir, "styles")
if !system.IsDir(stylesDir) {
t.Fatalf("expected styles directory to be created: %s", stylesDir)
}

// Validate INI syntax and keys
uCfg, err := ini.Load(cfgPath)
if err != nil {
t.Fatalf("failed to parse generated config with ini: %v", err)
}

coreSec := uCfg.Section("")
if coreSec.Key("StylesPath").String() != "styles" {
t.Errorf("expected StylesPath = styles, got %s", coreSec.Key("StylesPath").String())
}
if coreSec.Key("MinAlertLevel").String() != "suggestion" {
t.Errorf("expected MinAlertLevel = suggestion, got %s", coreSec.Key("MinAlertLevel").String())
}
if coreSec.Key("Packages").String() != "Google" {
t.Errorf("expected Packages = Google, got %s", coreSec.Key("Packages").String())
}

formatSec := uCfg.Section("*.{md,txt}")
if formatSec.Key("BasedOnStyles").String() != "Vale, Google" {
t.Errorf("expected BasedOnStyles = Vale, Google, got %s", formatSec.Key("BasedOnStyles").String())
}

// Validate with Vale pipeline loader
cfg, err := core.ReadPipeline(&core.CLIFlags{Path: cfgPath, IgnoreGlobal: true}, true)
if err != nil {
t.Fatalf("failed to read generated config through ReadPipeline: %v", err)
}

if cfg.MinAlertLevel != core.LevelToInt["suggestion"] {
t.Errorf("expected MinAlertLevel %d, got %d", core.LevelToInt["suggestion"], cfg.MinAlertLevel)
}

pkgs, err := core.GetPackages(cfgPath)
if err != nil {
t.Fatalf("failed to get packages from generated config: %v", err)
}
if len(pkgs) != 1 || pkgs[0] != "Google" {
t.Errorf("expected packages [Google], got %v", pkgs)
}

baseStyles := cfg.SBaseStyles["*.{md,txt}"]
if len(baseStyles) != 2 || baseStyles[0] != "Vale" || baseStyles[1] != "Google" {
t.Errorf("expected base styles [Vale Google], got %v", baseStyles)
}
}

func TestInitCmd_ExistingConfigFails(t *testing.T) {
tmpDir := t.TempDir()
cfgPath := filepath.Join(tmpDir, ".vale.ini")

err := os.WriteFile(cfgPath, []byte("StylesPath = styles\n"), 0o600)
if err != nil {
t.Fatalf("failed to create existing config: %v", err)
}

err = initCmd([]string{cfgPath}, &core.CLIFlags{})
if err == nil {
t.Fatal("expected initCmd to fail when config already exists, got nil")
}

if !strings.Contains(err.Error(), "already exists") {
t.Errorf("expected error message mentioning 'already exists', got: %v", err)
}
}

func TestInitCmd_AlternativeConfigInDirFails(t *testing.T) {
tmpDir := t.TempDir()
altPath := filepath.Join(tmpDir, "vale.ini")

err := os.WriteFile(altPath, []byte("StylesPath = styles\n"), 0o600)
if err != nil {
t.Fatalf("failed to create existing alt config: %v", err)
}

targetPath := filepath.Join(tmpDir, ".vale.ini")
err = initCmd([]string{targetPath}, &core.CLIFlags{})
if err == nil {
t.Fatal("expected initCmd to fail when alternative config exists in directory, got nil")
}

if !strings.Contains(err.Error(), "already exists") {
t.Errorf("expected error message mentioning 'already exists', got: %v", err)
}
}

func TestInitCmd_DefaultTargetInCwd(t *testing.T) {
tmpDir := t.TempDir()
origWd, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get current working directory: %v", err)
}
defer func() {
_ = os.Chdir(origWd)
}()

if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("failed to change directory to temp dir: %v", err)
}

err = initCmd(nil, &core.CLIFlags{})
if err != nil {
t.Fatalf("initCmd without args failed: %v", err)
}

expectedFile := filepath.Join(tmpDir, ".vale.ini")
if !system.FileExists(expectedFile) {
t.Fatalf("expected .vale.ini in cwd, file not found: %s", expectedFile)
}

expectedStyles := filepath.Join(tmpDir, "styles")
if !system.IsDir(expectedStyles) {
t.Fatalf("expected styles dir in cwd, dir not found: %s", expectedStyles)
}
}

func TestInitCmd_WithTargetDirectory(t *testing.T) {
tmpDir := t.TempDir()
subDir := filepath.Join(tmpDir, "subproject")
if err := os.MkdirAll(subDir, os.ModePerm); err != nil {
t.Fatalf("failed to create subproject dir: %v", err)
}

err := initCmd([]string{subDir}, &core.CLIFlags{})
if err != nil {
t.Fatalf("initCmd with directory argument failed: %v", err)
}

expectedFile := filepath.Join(subDir, ".vale.ini")
if !system.FileExists(expectedFile) {
t.Fatalf("expected .vale.ini inside subproject, file not found: %s", expectedFile)
}

expectedStyles := filepath.Join(subDir, "styles")
if !system.IsDir(expectedStyles) {
t.Fatalf("expected styles inside subproject, dir not found: %s", expectedStyles)
}
}

func TestInitCmd_WithConfigFlag(t *testing.T) {
tmpDir := t.TempDir()
cfgPath := filepath.Join(tmpDir, ".custom.ini")

err := initCmd(nil, &core.CLIFlags{Path: cfgPath})
if err != nil {
t.Fatalf("initCmd with --config flag failed: %v", err)
}

if !system.FileExists(cfgPath) {
t.Fatalf("expected custom config file to exist: %s", cfgPath)
}

expectedStyles := filepath.Join(tmpDir, "styles")
if !system.IsDir(expectedStyles) {
t.Fatalf("expected styles dir in config dir, dir not found: %s", expectedStyles)
}
}

func TestInitCmd_StylesDirAlreadyExists(t *testing.T) {
tmpDir := t.TempDir()
cfgPath := filepath.Join(tmpDir, ".vale.ini")
stylesDir := filepath.Join(tmpDir, "styles")

if err := os.MkdirAll(stylesDir, os.ModePerm); err != nil {
t.Fatalf("failed to pre-create styles dir: %v", err)
}

err := initCmd([]string{cfgPath}, &core.CLIFlags{})
if err != nil {
t.Fatalf("initCmd failed when styles dir already exists: %v", err)
}

if !system.FileExists(cfgPath) {
t.Fatalf("expected config file to be created: %s", cfgPath)
}
}

func TestInitCmd_StylesPathIsFileFails(t *testing.T) {
tmpDir := t.TempDir()
cfgPath := filepath.Join(tmpDir, ".vale.ini")
stylesConflict := filepath.Join(tmpDir, "styles")

if err := os.WriteFile(stylesConflict, []byte("conflict"), 0o600); err != nil {
t.Fatalf("failed to create conflicting styles file: %v", err)
}

err := initCmd([]string{cfgPath}, &core.CLIFlags{})
if err == nil {
t.Fatal("expected initCmd to fail when styles exists as regular file, got nil")
}
}

func TestInitCmd_ValidatesFixture(t *testing.T) {
fixturePath := filepath.Join("..", "..", "testdata", "fixtures", "init", ".vale.ini")
if !system.FileExists(fixturePath) {
t.Fatalf("expected fixture to exist: %s", fixturePath)
}

content, err := os.ReadFile(fixturePath)
if err != nil {
t.Fatalf("failed to read fixture: %v", err)
}

if string(content) != core.DefaultConfigTemplate {
t.Errorf("expected fixture content to match DefaultConfigTemplate")
}

cfg, err := core.ReadPipeline(&core.CLIFlags{Path: fixturePath, IgnoreGlobal: true}, true)
if err != nil {
t.Fatalf("failed to read fixture pipeline: %v", err)
}

if cfg.MinAlertLevel != core.LevelToInt["suggestion"] {
t.Errorf("expected MinAlertLevel %d, got %d", core.LevelToInt["suggestion"], cfg.MinAlertLevel)
}
}
22 changes: 22 additions & 0 deletions cmd/vale/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"fmt"
"io"
"os"
"path/filepath"

"github.com/pterm/pterm"
"github.com/spf13/pflag"

"github.com/vale-cli/vale/v3/internal/core"
Expand Down Expand Up @@ -82,6 +84,26 @@ func handleError(err error) {
os.Exit(2)
}

func initCmd(args []string, flags *core.CLIFlags) error {
target := ".vale.ini"
if len(args) > 0 {
target = args[0]
} else if flags.Path != "" {
target = flags.Path
}

if system.IsDir(target) {
target = filepath.Join(target, ".vale.ini")
}

if err := core.InitConfig(target); err != nil {
return err
}

pterm.Success.Println(fmt.Sprintf("Initialized configuration in '%s'.", target))
return nil
}

func main() {
// Every exit path below calls this explicitly: os.Exit skips deferred
// functions, so a defer here would silently drop the profile.
Expand Down
58 changes: 58 additions & 0 deletions internal/core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,61 @@ func (c *Config) SphinxNames(key string) []string {
}
return out
}

// DefaultConfigTemplate is the default configuration template written by `vale init`.
const DefaultConfigTemplate = `# Core settings
# StylesPath defines where Vale looks for styles and downloaded packages.
StylesPath = styles

# MinAlertLevel sets the minimum alert severity to display: suggestion, warning, or error.
MinAlertLevel = suggestion

# External packages installed via 'vale sync'.
Packages = Google

# Format-specific configuration
# Applies the Vale and Google styles to Markdown and text files.
[*.{md,txt}]
BasedOnStyles = Vale, Google
`

// InitConfig creates a default .vale.ini configuration file at target and
// scaffolds the styles directory.
// Returns an error if a configuration file already exists at target or
// within target's directory.
func InitConfig(target string) error {
if target == "" {
target = ".vale.ini"
} else if system.IsDir(target) {
target = filepath.Join(target, ".vale.ini")
}

if system.FileExists(target) {
return NewE100("init", fmt.Errorf("configuration file '%s' already exists", target))
}

dir := filepath.Dir(target)
for _, name := range configNames {
candidate := filepath.Join(dir, name)
if system.FileExists(candidate) {
return NewE100("init", fmt.Errorf("configuration file '%s' already exists", candidate))
}
}

if dir != "" && dir != "." && !system.IsDir(dir) {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return NewE100("init", fmt.Errorf("failed to create directory '%s': %w", dir, err))
}
}

if err := os.WriteFile(target, []byte(DefaultConfigTemplate), 0o644); err != nil {
return NewE100("init", fmt.Errorf("failed to write configuration file '%s': %w", target, err))
}

stylesDir := filepath.Join(dir, "styles")
if err := os.MkdirAll(stylesDir, os.ModePerm); err != nil {
return NewE100("init", fmt.Errorf("failed to create styles directory '%s': %w", stylesDir, err))
}

return nil
}
Loading
Loading