Compare commits

...
Sign in to create a new pull request.

3 commits
dev ... main

Author SHA1 Message Date
Claude
59b968b1ef
feat: upgrade to core v0.8.0-alpha.1, replace banned stdlib imports
Migrate from forge.lthn.ai deps to dappco.re. Replace os, strings,
path/filepath with Core primitives. OnStartup returns core.Result.
Service factory returns core.Result.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 14:06:13 +00:00
9a12aa8d7d Merge pull request '[agent/claude:sonnet] Fix these issues: 1) Update CLAUDE.md — replace outdated `...' (#2) from agent/fix-these-issues--1--update-claude-md into main 2026-03-17 08:02:25 +00:00
Snider
62c57e6eef fix(config): update CLAUDE.md commands and remove redundant fmt.Sprintf in coreerr.E calls
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 07:11:11 +00:00
7 changed files with 67 additions and 73 deletions

View file

@ -7,10 +7,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
This project uses the Core CLI (`core` binary), not `go` directly.
```bash
core go test # run all tests
core go test --run TestConfig_Get_Good # run a single test
core go cov # test with coverage
core go cov --open # coverage + open HTML report
go test ./... # run all tests
go test -run TestConfig_Get_Good ./... # run a single test
go test -cover ./... # test with coverage
core go qa # format, vet, lint, test
core go qa full # adds race detector, vuln scan, security audit

View file

@ -11,17 +11,12 @@
package config
import (
"fmt"
"iter"
"maps"
"os"
"path/filepath"
"strings"
"sync"
coreio "forge.lthn.ai/core/go-io"
coreerr "forge.lthn.ai/core/go-log"
core "forge.lthn.ai/core/go/pkg/core"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
)
@ -60,18 +55,23 @@ func WithEnvPrefix(prefix string) Option {
}
}
// dotReplacer implements viper.StringReplacer, converting dots to underscores
// for environment variable key mapping without importing strings directly.
type dotReplacer struct{}
func (dotReplacer) Replace(s string) string { return core.Replace(s, ".", "_") }
// New creates a new Config instance with the given options.
// If no medium is provided, it defaults to io.Local.
// If no path is provided, it defaults to ~/.core/config.yaml.
func New(opts ...Option) (*Config, error) {
c := &Config{
v: viper.New(),
v: viper.NewWithOptions(viper.EnvKeyReplacer(dotReplacer{})),
f: viper.New(),
}
// Configure viper defaults
c.v.SetEnvPrefix("CORE_CONFIG")
c.v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
for _, opt := range opts {
opt(c)
@ -82,11 +82,11 @@ func New(opts ...Option) (*Config, error) {
}
if c.path == "" {
home, err := os.UserHomeDir()
if err != nil {
return nil, coreerr.E("config.New", "failed to determine home directory", err)
home := core.Env("DIR_HOME")
if home == "" {
return nil, core.E("config.New", "failed to determine home directory", nil)
}
c.path = filepath.Join(home, ".core", "config.yaml")
c.path = core.Path(home, ".core", "config.yaml")
}
c.v.AutomaticEnv()
@ -94,7 +94,7 @@ func New(opts ...Option) (*Config, error) {
// Load existing config file if it exists
if c.medium.Exists(c.path) {
if err := c.LoadFile(c.medium, c.path); err != nil {
return nil, coreerr.E("config.New", "failed to load config file", err)
return nil, core.E("config.New", "failed to load config file", err)
}
}
@ -109,27 +109,27 @@ func (c *Config) LoadFile(m coreio.Medium, path string) error {
content, err := m.Read(path)
if err != nil {
return coreerr.E("config.LoadFile", fmt.Sprintf("failed to read config file: %s", path), err)
return core.E("config.LoadFile", "failed to read config file: "+path, err)
}
ext := filepath.Ext(path)
ext := core.PathExt(path)
configType := "yaml"
if ext == "" && filepath.Base(path) == ".env" {
if ext == "" && core.PathBase(path) == ".env" {
configType = "env"
} else if ext != "" {
configType = strings.TrimPrefix(ext, ".")
configType = core.TrimPrefix(ext, ".")
}
// Load into file-backed viper
c.f.SetConfigType(configType)
if err := c.f.MergeConfig(strings.NewReader(content)); err != nil {
return coreerr.E("config.LoadFile", fmt.Sprintf("failed to parse config file (f): %s", path), err)
if err := c.f.MergeConfig(core.NewReader(content)); err != nil {
return core.E("config.LoadFile", "failed to parse config file (f): "+path, err)
}
// Load into full viper
c.v.SetConfigType(configType)
if err := c.v.MergeConfig(strings.NewReader(content)); err != nil {
return coreerr.E("config.LoadFile", fmt.Sprintf("failed to parse config file (v): %s", path), err)
if err := c.v.MergeConfig(core.NewReader(content)); err != nil {
return core.E("config.LoadFile", "failed to parse config file (v): "+path, err)
}
return nil
@ -144,17 +144,17 @@ func (c *Config) Get(key string, out any) error {
if key == "" {
if err := c.v.Unmarshal(out); err != nil {
return coreerr.E("config.Get", "failed to unmarshal full config", err)
return core.E("config.Get", "failed to unmarshal full config", err)
}
return nil
}
if !c.v.IsSet(key) {
return coreerr.E("config.Get", fmt.Sprintf("key not found: %s", key), nil)
return core.E("config.Get", "key not found: "+key, nil)
}
if err := c.v.UnmarshalKey(key, out); err != nil {
return coreerr.E("config.Get", fmt.Sprintf("failed to unmarshal key: %s", key), err)
return core.E("config.Get", "failed to unmarshal key: "+key, err)
}
return nil
}
@ -178,7 +178,7 @@ func (c *Config) Commit() error {
defer c.mu.Unlock()
if err := Save(c.medium, c.path, c.f.AllSettings()); err != nil {
return coreerr.E("config.Commit", "failed to save config", err)
return core.E("config.Commit", "failed to save config", err)
}
return nil
}
@ -201,13 +201,13 @@ func (c *Config) Path() string {
func Load(m coreio.Medium, path string) (map[string]any, error) {
content, err := m.Read(path)
if err != nil {
return nil, coreerr.E("config.Load", "failed to read config file: "+path, err)
return nil, core.E("config.Load", "failed to read config file: "+path, err)
}
v := viper.New()
v.SetConfigType("yaml")
if err := v.ReadConfig(strings.NewReader(content)); err != nil {
return nil, coreerr.E("config.Load", "failed to parse config file: "+path, err)
if err := v.ReadConfig(core.NewReader(content)); err != nil {
return nil, core.E("config.Load", "failed to parse config file: "+path, err)
}
return v.AllSettings(), nil
@ -218,20 +218,18 @@ func Load(m coreio.Medium, path string) (map[string]any, error) {
func Save(m coreio.Medium, path string, data map[string]any) error {
out, err := yaml.Marshal(data)
if err != nil {
return coreerr.E("config.Save", "failed to marshal config", err)
return core.E("config.Save", "failed to marshal config", err)
}
dir := filepath.Dir(path)
dir := core.PathDir(path)
if err := m.EnsureDir(dir); err != nil {
return coreerr.E("config.Save", "failed to create config directory: "+dir, err)
return core.E("config.Save", "failed to create config directory: "+dir, err)
}
if err := m.Write(path, string(out)); err != nil {
return coreerr.E("config.Save", "failed to write config file: "+path, err)
return core.E("config.Save", "failed to write config file: "+path, err)
}
return nil
}
// Ensure Config implements core.Config at compile time.
var _ core.Config = (*Config)(nil)

View file

@ -2,10 +2,10 @@ package config
import (
"maps"
"os"
"testing"
coreio "forge.lthn.ai/core/go-io"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
"github.com/stretchr/testify/assert"
)
@ -185,7 +185,7 @@ func TestConfig_DefaultPath_Good(t *testing.T) {
cfg, err := New(WithMedium(m))
assert.NoError(t, err)
home, _ := os.UserHomeDir()
home := core.Env("DIR_HOME")
assert.Equal(t, home+"/.core/config.yaml", cfg.Path())
}

13
env.go
View file

@ -3,7 +3,8 @@ package config
import (
"iter"
"os"
"strings"
core "dappco.re/go/core"
)
// Env returns an iterator over environment variables with the given prefix,
@ -15,11 +16,11 @@ import (
func Env(prefix string) iter.Seq2[string, any] {
return func(yield func(string, any) bool) {
for _, env := range os.Environ() {
if !strings.HasPrefix(env, prefix) {
if !core.HasPrefix(env, prefix) {
continue
}
parts := strings.SplitN(env, "=", 2)
parts := core.SplitN(env, "=", 2)
if len(parts) != 2 {
continue
}
@ -28,9 +29,9 @@ func Env(prefix string) iter.Seq2[string, any] {
value := parts[1]
// Strip prefix and convert to dot notation
key := strings.TrimPrefix(name, prefix)
key = strings.ToLower(key)
key = strings.ReplaceAll(key, "_", ".")
key := core.TrimPrefix(name, prefix)
key = core.Lower(key)
key = core.Replace(key, "_", ".")
if !yield(key, value) {
return

6
go.mod
View file

@ -3,15 +3,15 @@ module forge.lthn.ai/core/config
go 1.26.0
require (
forge.lthn.ai/core/go v0.3.1
forge.lthn.ai/core/go-io v0.1.5
forge.lthn.ai/core/go-log v0.0.4
dappco.re/go/core v0.8.0-alpha.1
dappco.re/go/core/io v0.2.0
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
gopkg.in/yaml.v3 v3.0.1
)
require (
forge.lthn.ai/core/go-log v0.0.4 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect

8
go.sum
View file

@ -1,7 +1,7 @@
forge.lthn.ai/core/go v0.3.1 h1:5FMTsUhLcxSr07F9q3uG0Goy4zq4eLivoqi8shSY4UM=
forge.lthn.ai/core/go v0.3.1/go.mod h1:gE6c8h+PJ2287qNhVUJ5SOe1kopEwHEquvinstpuyJc=
forge.lthn.ai/core/go-io v0.1.5 h1:+XJ1YhaGGFLGtcNbPtVlndTjk+pO0Ydi2hRDj5/cHOM=
forge.lthn.ai/core/go-io v0.1.5/go.mod h1:FRtXSsi8W+U9vewCU+LBAqqbIj3wjXA4dBdSv3SAtWI=
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=

View file

@ -3,9 +3,8 @@ package config
import (
"context"
coreio "forge.lthn.ai/core/go-io"
coreerr "forge.lthn.ai/core/go-log"
core "forge.lthn.ai/core/go/pkg/core"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
)
// Service wraps Config as a framework service with lifecycle support.
@ -24,16 +23,16 @@ type ServiceOptions struct {
// NewConfigService creates a new config service factory for the Core framework.
// Register it with core.WithService(config.NewConfigService).
func NewConfigService(c *core.Core) (any, error) {
func NewConfigService(c *core.Core) core.Result {
svc := &Service{
ServiceRuntime: core.NewServiceRuntime(c, ServiceOptions{}),
}
return svc, nil
return core.Result{Value: svc, OK: true}
}
// OnStartup loads the configuration file during application startup.
func (s *Service) OnStartup(_ context.Context) error {
opts := s.Opts()
func (s *Service) OnStartup(_ context.Context) core.Result {
opts := s.Options()
var configOpts []Option
if opts.Path != "" {
@ -45,17 +44,17 @@ func (s *Service) OnStartup(_ context.Context) error {
cfg, err := New(configOpts...)
if err != nil {
return coreerr.E("config.Service.OnStartup", "failed to create config", err)
return core.Result{Value: core.E("config.Service.OnStartup", "failed to create config", err)}
}
s.config = cfg
return nil
return core.Result{OK: true}
}
// Get retrieves a configuration value by key.
func (s *Service) Get(key string, out any) error {
if s.config == nil {
return coreerr.E("config.Service.Get", "config not loaded", nil)
return core.E("config.Service.Get", "config not loaded", nil)
}
return s.config.Get(key, out)
}
@ -63,7 +62,7 @@ func (s *Service) Get(key string, out any) error {
// Set stores a configuration value by key.
func (s *Service) Set(key string, v any) error {
if s.config == nil {
return coreerr.E("config.Service.Set", "config not loaded", nil)
return core.E("config.Service.Set", "config not loaded", nil)
}
return s.config.Set(key, v)
}
@ -71,7 +70,7 @@ func (s *Service) Set(key string, v any) error {
// Commit persists any configuration changes to disk.
func (s *Service) Commit() error {
if s.config == nil {
return coreerr.E("config.Service.Commit", "config not loaded", nil)
return core.E("config.Service.Commit", "config not loaded", nil)
}
return s.config.Commit()
}
@ -79,13 +78,10 @@ func (s *Service) Commit() error {
// LoadFile merges a configuration file into the central configuration.
func (s *Service) LoadFile(m coreio.Medium, path string) error {
if s.config == nil {
return coreerr.E("config.Service.LoadFile", "config not loaded", nil)
return core.E("config.Service.LoadFile", "config not loaded", nil)
}
return s.config.LoadFile(m, path)
}
// Ensure Service implements core.Config and Startable at compile time.
var (
_ core.Config = (*Service)(nil)
_ core.Startable = (*Service)(nil)
)
// Ensure Service implements core.Startable at compile time.
var _ core.Startable = (*Service)(nil)