diff --git a/core.go b/core.go deleted file mode 100644 index 96b3bb1..0000000 --- a/core.go +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -// Package core is the Core framework for Go. -// -// Single import, single struct, everything accessible: -// -// import core "forge.lthn.ai/core/go" -// -// c, _ := core.New( -// core.WithAssets(myEmbed), -// core.WithService(myFactory), -// ) -// -// // DI -// svc, _ := core.ServiceFor[*MyService](c, "name") -// -// // Mount -// content, _ := c.Mnt().ReadString("persona/secops/developer.md") -// c.Mnt().Extract(targetDir, data) -// -// // IPC -// c.ACTION(msg) -package core - -import ( - di "forge.lthn.ai/core/go/pkg/core" -) - -// --- Types --- - -// Core is the central application container. -type Core = di.Core - -// Option configures a Core instance. -type Option = di.Option - -// Message is the IPC message type. -type Message = di.Message - -// Sub is a scoped view of an embedded filesystem. -type Sub = di.Sub - -// ExtractOptions configures template extraction. -type ExtractOptions = di.ExtractOptions - -// Startable is implemented by services with startup logic. -type Startable = di.Startable - -// Stoppable is implemented by services with shutdown logic. -type Stoppable = di.Stoppable - -// LocaleProvider provides locale filesystems for i18n. -type LocaleProvider = di.LocaleProvider - -// ServiceRuntime is the base for services with typed options. -type ServiceRuntime[T any] = di.ServiceRuntime[T] - -// --- Constructor + Options --- - -// New creates a new Core instance. -var New = di.New - -// WithService registers a service factory. -var WithService = di.WithService - -// WithName registers a named service factory. -var WithName = di.WithName - -// WithAssets mounts an embedded filesystem. -var WithAssets = di.WithAssets - -// WithMount mounts an embedded filesystem at a subdirectory. -var WithMount = di.WithMount - -// WithServiceLock prevents late service registration. -var WithServiceLock = di.WithServiceLock - -// WithApp sets the GUI runtime. -var WithApp = di.WithApp - -// Mount creates a scoped view of an embed.FS at basedir. -var Mount = di.Mount - -// --- Generic Functions --- - -// ServiceFor retrieves a typed service by name. -func ServiceFor[T any](c *Core, name string) (T, error) { - return di.ServiceFor[T](c, name) -} - -// E creates a structured error. -var E = di.E - -// --- Configuration (core.Etc) --- - -// Etc is the configuration and feature flags store. -type Etc = di.Etc - -// NewEtc creates a standalone configuration store. -var NewEtc = di.NewEtc - -// Var is a typed optional variable (set/unset/get). -type Var[T any] = di.Var[T] - -// NewVar creates a Var with the given value. -func NewVar[T any](val T) Var[T] { - return di.NewVar(val) -} diff --git a/go.mod b/go.mod index 2620c9e..e6140ad 100644 --- a/go.mod +++ b/go.mod @@ -2,14 +2,13 @@ module forge.lthn.ai/core/go go 1.26.0 -require ( - forge.lthn.ai/core/go-io v0.1.6 - forge.lthn.ai/core/go-log v0.0.4 - github.com/stretchr/testify v1.11.1 -) +require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b83e5f7..5a10c39 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,17 @@ -forge.lthn.ai/core/go-io v0.1.6 h1:RByYeP829HFqR2yLg5iBM5dGHKzPFYc+udl/Y1DZIRs= -forge.lthn.ai/core/go-io v0.1.6/go.mod h1:3MSuQZuzhCi6aefECQ/LxhM8ooVLam1KgEvgeEjYZVc= -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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= diff --git a/pkg/mnt/extract.go b/pkg/mnt/extract.go deleted file mode 100644 index 797f983..0000000 --- a/pkg/mnt/extract.go +++ /dev/null @@ -1,194 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -package mnt - -import ( - "bytes" - "io" - "io/fs" - "os" - "path/filepath" - "strings" - "text/template" -) - -// ExtractOptions configures template extraction. -type ExtractOptions struct { - // TemplateFilters identifies template files by substring match. - // Default: [".tmpl"] - TemplateFilters []string - - // IgnoreFiles is a set of filenames to skip during extraction. - IgnoreFiles map[string]struct{} - - // RenameFiles maps original filenames to new names. - RenameFiles map[string]string -} - -// Extract copies a template directory from an fs.FS to targetDir, -// processing Go text/template in filenames and file contents. -// -// Files containing a template filter substring (default: ".tmpl") have -// their contents processed through text/template with the given data. -// The filter is stripped from the output filename. -// -// Directory and file names can contain Go template expressions: -// {{.Name}}/main.go → myproject/main.go -// -// Data can be any struct or map[string]string for template substitution. -func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) error { - opt := ExtractOptions{ - TemplateFilters: []string{".tmpl"}, - IgnoreFiles: make(map[string]struct{}), - RenameFiles: make(map[string]string), - } - if len(opts) > 0 { - if len(opts[0].TemplateFilters) > 0 { - opt.TemplateFilters = opts[0].TemplateFilters - } - if opts[0].IgnoreFiles != nil { - opt.IgnoreFiles = opts[0].IgnoreFiles - } - if opts[0].RenameFiles != nil { - opt.RenameFiles = opts[0].RenameFiles - } - } - - // Ensure target directory exists - targetDir, err := filepath.Abs(targetDir) - if err != nil { - return err - } - if err := os.MkdirAll(targetDir, 0755); err != nil { - return err - } - - // Categorise files - var dirs []string - var templateFiles []string - var standardFiles []string - - err = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if path == "." { - return nil - } - if d.IsDir() { - dirs = append(dirs, path) - return nil - } - filename := filepath.Base(path) - if _, ignored := opt.IgnoreFiles[filename]; ignored { - return nil - } - if isTemplate(filename, opt.TemplateFilters) { - templateFiles = append(templateFiles, path) - } else { - standardFiles = append(standardFiles, path) - } - return nil - }) - if err != nil { - return err - } - - // Create directories (names may contain templates) - for _, dir := range dirs { - target := renderPath(filepath.Join(targetDir, dir), data) - if err := os.MkdirAll(target, 0755); err != nil { - return err - } - } - - // Process template files - for _, path := range templateFiles { - tmpl, err := template.ParseFS(fsys, path) - if err != nil { - return err - } - - targetFile := renderPath(filepath.Join(targetDir, path), data) - - // Strip template filters from filename - dir := filepath.Dir(targetFile) - name := filepath.Base(targetFile) - for _, filter := range opt.TemplateFilters { - name = strings.ReplaceAll(name, filter, "") - } - if renamed := opt.RenameFiles[name]; renamed != "" { - name = renamed - } - targetFile = filepath.Join(dir, name) - - f, err := os.Create(targetFile) - if err != nil { - return err - } - if err := tmpl.Execute(f, data); err != nil { - f.Close() - return err - } - f.Close() - } - - // Copy standard files - for _, path := range standardFiles { - name := filepath.Base(path) - if renamed := opt.RenameFiles[name]; renamed != "" { - path = filepath.Join(filepath.Dir(path), renamed) - } - target := renderPath(filepath.Join(targetDir, path), data) - if err := copyFile(fsys, path, target); err != nil { - return err - } - } - - return nil -} - -func isTemplate(filename string, filters []string) bool { - for _, f := range filters { - if strings.Contains(filename, f) { - return true - } - } - return false -} - -func renderPath(path string, data any) string { - if data == nil { - return path - } - tmpl, err := template.New("path").Parse(path) - if err != nil { - return path - } - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return path - } - return buf.String() -} - -func copyFile(fsys fs.FS, source, target string) error { - s, err := fsys.Open(source) - if err != nil { - return err - } - defer s.Close() - - if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { - return err - } - - d, err := os.Create(target) - if err != nil { - return err - } - defer d.Close() - - _, err = io.Copy(d, s) - return err -} diff --git a/pkg/mnt/mnt.go b/pkg/mnt/mnt.go deleted file mode 100644 index 5acb4f6..0000000 --- a/pkg/mnt/mnt.go +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -// Package mnt provides mount operations for the Core framework. -// -// Mount operations attach data to/from binaries and watch live filesystems: -// -// - FS: mount an embed.FS subdirectory for scoped access -// - Extract: extract a template directory with variable substitution -// - Watch: observe filesystem changes (file watcher) -// -// Zero external dependencies. All operations use stdlib only. -// -// Usage: -// -// sub, _ := mnt.FS(myEmbed, "lib/persona") -// content, _ := sub.ReadFile("secops/developer.md") -// -// mnt.Extract(sub, "/tmp/workspace", map[string]string{"Name": "myproject"}) -package mnt - -import ( - "embed" - "io/fs" - "path/filepath" -) - -// Sub wraps an embed.FS with a basedir for scoped access. -// All paths are relative to basedir. -type Sub struct { - basedir string - fs embed.FS -} - -// FS creates a scoped view of an embed.FS anchored at basedir. -// Returns error if basedir doesn't exist in the embedded filesystem. -func FS(efs embed.FS, basedir string) (*Sub, error) { - s := &Sub{fs: efs, basedir: basedir} - // Verify the basedir exists - if _, err := s.ReadDir("."); err != nil { - return nil, err - } - return s, nil -} - -func (s *Sub) path(name string) string { - return filepath.ToSlash(filepath.Join(s.basedir, name)) -} - -// Open opens the named file for reading. -func (s *Sub) Open(name string) (fs.File, error) { - return s.fs.Open(s.path(name)) -} - -// ReadDir reads the named directory. -func (s *Sub) ReadDir(name string) ([]fs.DirEntry, error) { - return s.fs.ReadDir(s.path(name)) -} - -// ReadFile reads the named file. -func (s *Sub) ReadFile(name string) ([]byte, error) { - return s.fs.ReadFile(s.path(name)) -} - -// ReadString reads the named file as a string. -func (s *Sub) ReadString(name string) (string, error) { - data, err := s.ReadFile(name) - if err != nil { - return "", err - } - return string(data), nil -} - -// Sub returns a new Sub anchored at a subdirectory within this Sub. -func (s *Sub) Sub(subDir string) (*Sub, error) { - return FS(s.fs, s.path(subDir)) -} - -// Embed returns the underlying embed.FS. -func (s *Sub) Embed() embed.FS { - return s.fs -} - -// BaseDir returns the basedir this Sub is anchored at. -func (s *Sub) BaseDir() string { - return s.basedir -} diff --git a/pkg/mnt/mnt_test.go b/pkg/mnt/mnt_test.go deleted file mode 100644 index 0d65e38..0000000 --- a/pkg/mnt/mnt_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2 - -package mnt_test - -import ( - "embed" - "os" - "testing" - - "forge.lthn.ai/core/go/pkg/mnt" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -//go:embed testdata -var testFS embed.FS - -func TestFS_Good(t *testing.T) { - sub, err := mnt.FS(testFS, "testdata") - require.NoError(t, err) - assert.Equal(t, "testdata", sub.BaseDir()) -} - -func TestFS_Bad_InvalidDir(t *testing.T) { - _, err := mnt.FS(testFS, "nonexistent") - assert.Error(t, err) -} - -func TestSub_ReadFile_Good(t *testing.T) { - sub, err := mnt.FS(testFS, "testdata") - require.NoError(t, err) - - data, err := sub.ReadFile("hello.txt") - require.NoError(t, err) - assert.Equal(t, "hello world\n", string(data)) -} - -func TestSub_ReadString_Good(t *testing.T) { - sub, err := mnt.FS(testFS, "testdata") - require.NoError(t, err) - - content, err := sub.ReadString("hello.txt") - require.NoError(t, err) - assert.Equal(t, "hello world\n", content) -} - -func TestSub_ReadDir_Good(t *testing.T) { - sub, err := mnt.FS(testFS, "testdata") - require.NoError(t, err) - - entries, err := sub.ReadDir(".") - require.NoError(t, err) - assert.True(t, len(entries) >= 1) -} - -func TestSub_Sub_Good(t *testing.T) { - sub, err := mnt.FS(testFS, "testdata") - require.NoError(t, err) - - nested, err := sub.Sub("subdir") - require.NoError(t, err) - - content, err := nested.ReadString("nested.txt") - require.NoError(t, err) - assert.Equal(t, "nested content\n", content) -} - -func TestExtract_Good(t *testing.T) { - sub, err := mnt.FS(testFS, "testdata/template") - require.NoError(t, err) - - targetDir := t.TempDir() - err = mnt.Extract(sub, targetDir, map[string]string{ - "Name": "myproject", - "Module": "forge.lthn.ai/core/myproject", - }) - require.NoError(t, err) - - // Check template was processed - readme, err := os.ReadFile(targetDir + "/README.md") - require.NoError(t, err) - assert.Contains(t, string(readme), "myproject project") - - // Check go.mod template was processed - gomod, err := os.ReadFile(targetDir + "/go.mod") - require.NoError(t, err) - assert.Contains(t, string(gomod), "module forge.lthn.ai/core/myproject") - - // Check non-template was copied as-is - main, err := os.ReadFile(targetDir + "/main.go") - require.NoError(t, err) - assert.Equal(t, "package main\n", string(main)) -} - -func TestExtract_Good_NoData(t *testing.T) { - sub, err := mnt.FS(testFS, "testdata") - require.NoError(t, err) - - targetDir := t.TempDir() - err = mnt.Extract(sub, targetDir, nil) - require.NoError(t, err) - - data, err := os.ReadFile(targetDir + "/hello.txt") - require.NoError(t, err) - assert.Equal(t, "hello world\n", string(data)) -} diff --git a/pkg/mnt/testdata/hello.txt b/pkg/mnt/testdata/hello.txt deleted file mode 100644 index 3b18e51..0000000 --- a/pkg/mnt/testdata/hello.txt +++ /dev/null @@ -1 +0,0 @@ -hello world diff --git a/pkg/mnt/testdata/subdir/nested.txt b/pkg/mnt/testdata/subdir/nested.txt deleted file mode 100644 index ca281f5..0000000 --- a/pkg/mnt/testdata/subdir/nested.txt +++ /dev/null @@ -1 +0,0 @@ -nested content diff --git a/pkg/mnt/testdata/template/README.md.tmpl b/pkg/mnt/testdata/template/README.md.tmpl deleted file mode 100644 index fdc89c8..0000000 --- a/pkg/mnt/testdata/template/README.md.tmpl +++ /dev/null @@ -1 +0,0 @@ -{{.Name}} project diff --git a/pkg/mnt/testdata/template/go.mod.tmpl b/pkg/mnt/testdata/template/go.mod.tmpl deleted file mode 100644 index 9f840df..0000000 --- a/pkg/mnt/testdata/template/go.mod.tmpl +++ /dev/null @@ -1 +0,0 @@ -module {{.Module}} diff --git a/pkg/mnt/testdata/template/main.go b/pkg/mnt/testdata/template/main.go deleted file mode 100644 index 06ab7d0..0000000 --- a/pkg/mnt/testdata/template/main.go +++ /dev/null @@ -1 +0,0 @@ -package main