ajhahn.de
← eeco
Go 179 lines
package main

import (
	"fmt"
	"io"
	"path/filepath"
	"strings"

	"github.com/ajhahnde/eeco/internal/config"
)

const configUsage = `usage:
  eeco config list                       show every config key, its effective value, and origin
  eeco config get <key>                  print the effective value of one key
  eeco config set <key> <value>          set a key in this workspace (<repo>/<user>/.eeco/config.local)
  eeco config set --global <key> <value> set a key in the user-global layer (~/.config/eeco/config.local)
  eeco config import [--force] <path>    copy config.local, cockpit.json, and workflows from another eeco project
Resolution order: built-in defaults → user-global → workspace (later wins).
The global layer lets every project inherit your settings; set it once with --global.
Exit 0 clean, 1 on a failure, 2 on usage error.`

// runConfig dispatches `eeco config` subcommands. Config has three
// layers — defaults, the user-global file, and the per-workspace
// config.local — and this group inspects and edits the two file layers.
func runConfig(args []string, stdout, stderr io.Writer) int {
	if len(args) == 0 {
		fmt.Fprintln(stderr, configUsage)
		return 2
	}
	switch args[0] {
	case "list":
		return runConfigList(args[1:], stdout, stderr)
	case "get":
		return runConfigGet(args[1:], stdout, stderr)
	case "set":
		return runConfigSet(args[1:], stdout, stderr)
	case "import":
		return runConfigImport(args[1:], stdout, stderr)
	default:
		fmt.Fprintln(stderr, configUsage)
		return 2
	}
}

// runConfigImport copies portable settings from another eeco project's
// workspace into this one. Without --force, existing local files and keys are
// preserved; with --force, the source wins.
func runConfigImport(args []string, stdout, stderr io.Writer) int {
	force, rest := popBoolFlag(args, "force")
	if len(rest) != 1 {
		fmt.Fprintln(stderr, "eeco config: import needs exactly one <path>")
		return 2
	}
	dst, code := loadInitedConfig(stderr, "eeco config")
	if code != 0 {
		return code
	}
	src, err := resolveSourceWorkspace(rest[0])
	if err != nil {
		fmt.Fprintln(stderr, "eeco config:", err)
		return 1
	}
	if sameWorkspace(src.Workspace, dst.Workspace) {
		fmt.Fprintln(stderr, "eeco config: source and destination are the same workspace")
		return 1
	}
	rep, err := importSettings(src, dst, force)
	if err != nil {
		fmt.Fprintln(stderr, "eeco config:", err)
		return 1
	}
	printImportReport(stdout, rest[0], rep)
	return 0
}

// runConfigList prints every known key with its effective value and the
// layer that set it (default | global | local), git-config-style.
func runConfigList(args []string, stdout, stderr io.Writer) int {
	if len(args) != 0 {
		fmt.Fprintln(stderr, configUsage)
		return 2
	}
	cfg, code := loadRepoConfig(stderr, "eeco config")
	if code != 0 {
		return code
	}
	globalKeys, err := config.DeclaredKeys(config.GlobalConfigLocalPath())
	if err != nil {
		fmt.Fprintln(stderr, "eeco config:", err)
		return 1
	}
	localKeys, err := config.DeclaredKeys(filepath.Join(cfg.Workspace, config.LocalFilename))
	if err != nil {
		fmt.Fprintln(stderr, "eeco config:", err)
		return 1
	}

	keys := config.KnownKeys()
	width := 0
	for _, k := range keys {
		if len(k) > width {
			width = len(k)
		}
	}
	for _, k := range keys {
		val, _ := config.EffectiveValue(cfg, k)
		origin := "default"
		switch {
		case localKeys[k]:
			origin = "local"
		case globalKeys[k]:
			origin = "global"
		}
		fmt.Fprintf(stdout, "%-*s = %s  (%s)\n", width, k, val, origin)
	}
	return 0
}

// runConfigGet prints the effective value of one key (bare, for scripts).
func runConfigGet(args []string, stdout, stderr io.Writer) int {
	if len(args) != 1 {
		fmt.Fprintln(stderr, "eeco config: get needs exactly one <key>")
		return 2
	}
	key := args[0]
	cfg, code := loadRepoConfig(stderr, "eeco config")
	if code != 0 {
		return code
	}
	val, ok := config.EffectiveValue(cfg, key)
	if !ok {
		fmt.Fprintf(stderr, "eeco config: unknown config key %q (run `eeco config list`)\n", key)
		return 1
	}
	fmt.Fprintln(stdout, val)
	return 0
}

// runConfigSet writes one key into the workspace config.local, or into
// the user-global config.local with --global. The value is validated
// against the same rules Load applies before anything is written.
func runConfigSet(args []string, stdout, stderr io.Writer) int {
	global, rest := popBoolFlag(args, "global")
	var key, val string
	switch {
	case len(rest) == 2:
		key, val = rest[0], rest[1]
	case len(rest) == 1 && strings.Contains(rest[0], "="):
		key, val, _ = strings.Cut(rest[0], "=")
	default:
		fmt.Fprintln(stderr, "eeco config: set needs <key> <value> (or key=value)")
		return 2
	}
	if err := config.ValidateSetValue(key, val); err != nil {
		fmt.Fprintln(stderr, "eeco config:", err)
		return 1
	}

	if global {
		if err := config.WriteGlobalKeys(map[string]string{key: val}); err != nil {
			fmt.Fprintln(stderr, "eeco config:", err)
			return 1
		}
		fmt.Fprintf(stdout, "eeco config: set %s=%s (global: %s)\n", key, val, config.GlobalConfigLocalPath())
		return 0
	}

	cfg, code := loadInitedConfig(stderr, "eeco config")
	if code != 0 {
		return code
	}
	if err := config.WriteLocalKeys(cfg, map[string]string{key: val}); err != nil {
		fmt.Fprintln(stderr, "eeco config:", err)
		return 1
	}
	fmt.Fprintf(stdout, "eeco config: set %s=%s (workspace)\n", key, val)
	return 0
}