From feaa4dec5e829260f8949f9bf133367a6ad3053f Mon Sep 17 00:00:00 2001 From: Virgil Date: Sun, 29 Mar 2026 23:01:10 +0000 Subject: [PATCH] fix(setup): make setup APIs AX-native Co-Authored-By: Virgil --- pkg/setup/config.go | 40 +++++++----- pkg/setup/config_example_test.go | 12 ++-- pkg/setup/config_test.go | 74 +++++++++++----------- pkg/setup/setup.go | 103 +++++++++++++++++++------------ pkg/setup/setup_test.go | 34 +++++----- 5 files changed, 153 insertions(+), 110 deletions(-) diff --git a/pkg/setup/config.go b/pkg/setup/config.go index 789bd12..2a5cf01 100644 --- a/pkg/setup/config.go +++ b/pkg/setup/config.go @@ -50,8 +50,9 @@ type configValue struct { // GenerateBuildConfig renders `build.yaml` content for a detected repo type. // -// content, err := setup.GenerateBuildConfig("/srv/repos/agent", setup.TypeGo) -func GenerateBuildConfig(path string, projType ProjectType) (string, error) { +// r := setup.GenerateBuildConfig("/srv/repos/agent", setup.TypeGo) +// if r.OK { content := r.Value.(string) } +func GenerateBuildConfig(path string, projType ProjectType) core.Result { name := core.PathBase(path) sections := []configSection{ { @@ -96,8 +97,9 @@ func GenerateBuildConfig(path string, projType ProjectType) (string, error) { // GenerateTestConfig renders `test.yaml` content for a detected repo type. // -// content, err := setup.GenerateTestConfig(setup.TypeGo) -func GenerateTestConfig(projType ProjectType) (string, error) { +// r := setup.GenerateTestConfig(setup.TypeGo) +// if r.OK { content := r.Value.(string) } +func GenerateTestConfig(projType ProjectType) core.Result { var sections []configSection switch projType { @@ -137,7 +139,7 @@ func GenerateTestConfig(projType ProjectType) (string, error) { return renderConfig("Test configuration", sections) } -func renderConfig(comment string, sections []configSection) (string, error) { +func renderConfig(comment string, sections []configSection) core.Result { builder := core.NewBuilder() if comment != "" { @@ -151,15 +153,19 @@ func renderConfig(comment string, sections []configSection) (string, error) { builder.WriteString(":\n") for _, value := range section.Values { - scalar, err := marshalConfigValue(value.Value) - if err != nil { - return "", core.E("setup.renderConfig", core.Concat("marshal ", section.Key, ".", value.Key), err) + scalar := marshalConfigValue(value.Value) + if !scalar.OK { + err, _ := scalar.Value.(error) + return core.Result{ + Value: core.E("setup.renderConfig", core.Concat("marshal ", section.Key, ".", value.Key), err), + OK: false, + } } builder.WriteString(" ") builder.WriteString(value.Key) builder.WriteString(": ") - builder.WriteString(scalar) + builder.WriteString(scalar.Value.(string)) builder.WriteString("\n") } @@ -168,21 +174,27 @@ func renderConfig(comment string, sections []configSection) (string, error) { } } - return builder.String(), nil + return core.Result{Value: builder.String(), OK: true} } -func marshalConfigValue(value any) (scalar string, err error) { +func marshalConfigValue(value any) (result core.Result) { defer func() { if recovered := recover(); recovered != nil { - err = core.E("setup.marshalConfigValue", core.Sprint(recovered), nil) + result = core.Result{ + Value: core.E("setup.marshalConfigValue", core.Sprint(recovered), nil), + OK: false, + } } }() data, err := yaml.Marshal(value) if err != nil { - return "", err + return core.Result{ + Value: core.E("setup.marshalConfigValue", "yaml marshal value", err), + OK: false, + } } - return core.Trim(string(data)), nil + return core.Result{Value: core.Trim(string(data)), OK: true} } func parseGitRemote(remote string) string { diff --git a/pkg/setup/config_example_test.go b/pkg/setup/config_example_test.go index a328f2d..719b4f3 100644 --- a/pkg/setup/config_example_test.go +++ b/pkg/setup/config_example_test.go @@ -10,18 +10,18 @@ func ExampleGenerateBuildConfig() { dir := (&core.Fs{}).NewUnrestricted().TempDir("example") defer (&core.Fs{}).NewUnrestricted().DeleteAll(dir) - config, err := GenerateBuildConfig(dir, TypeGo) - core.Println(err == nil) - core.Println(core.Contains(config, "type: go")) + config := GenerateBuildConfig(dir, TypeGo) + core.Println(config.OK) + core.Println(core.Contains(config.Value.(string), "type: go")) // Output: // true // true } func ExampleGenerateTestConfig() { - config, err := GenerateTestConfig(TypeGo) - core.Println(err == nil) - core.Println(core.Contains(config, "go test")) + config := GenerateTestConfig(TypeGo) + core.Println(config.OK) + core.Println(core.Contains(config.Value.(string), "go test")) // Output: // true // true diff --git a/pkg/setup/config_test.go b/pkg/setup/config_test.go index 9d29d29..ed61d87 100644 --- a/pkg/setup/config_test.go +++ b/pkg/setup/config_test.go @@ -10,47 +10,51 @@ import ( ) func TestConfig_GenerateBuildConfig_Good_Go(t *testing.T) { - config, err := GenerateBuildConfig("/tmp/myapp", TypeGo) - require.NoError(t, err) - assert.Contains(t, config, "# myapp build configuration") - assert.Contains(t, config, "type: go") - assert.Contains(t, config, "name: myapp") - assert.Contains(t, config, "main: ./cmd/myapp") - assert.Contains(t, config, "cgo: false") + config := GenerateBuildConfig("/tmp/myapp", TypeGo) + require.True(t, config.OK) + text := config.Value.(string) + assert.Contains(t, text, "# myapp build configuration") + assert.Contains(t, text, "type: go") + assert.Contains(t, text, "name: myapp") + assert.Contains(t, text, "main: ./cmd/myapp") + assert.Contains(t, text, "cgo: false") } func TestConfig_GenerateBuildConfig_Bad_Unknown(t *testing.T) { - config, err := GenerateBuildConfig("/tmp/myapp", TypeUnknown) - require.NoError(t, err) - assert.NotEmpty(t, config) + config := GenerateBuildConfig("/tmp/myapp", TypeUnknown) + require.True(t, config.OK) + assert.NotEmpty(t, config.Value.(string)) } func TestConfig_GenerateBuildConfig_Ugly_WailsNestedPath(t *testing.T) { - config, err := GenerateBuildConfig("/tmp/workspaces/team-console", TypeWails) - require.NoError(t, err) - assert.Contains(t, config, "name: team-console") - assert.Contains(t, config, "type: wails") - assert.Contains(t, config, "main: ./cmd/team-console") + config := GenerateBuildConfig("/tmp/workspaces/team-console", TypeWails) + require.True(t, config.OK) + text := config.Value.(string) + assert.Contains(t, text, "name: team-console") + assert.Contains(t, text, "type: wails") + assert.Contains(t, text, "main: ./cmd/team-console") } func TestConfig_GenerateTestConfig_Good_Go(t *testing.T) { - config, err := GenerateTestConfig(TypeGo) - require.NoError(t, err) - assert.Contains(t, config, "go test") + config := GenerateTestConfig(TypeGo) + require.True(t, config.OK) + assert.Contains(t, config.Value.(string), "go test") } func TestConfig_GenerateTestConfig_Bad_Unknown(t *testing.T) { - config, err := GenerateTestConfig(TypeUnknown) - require.NoError(t, err) - assert.Contains(t, config, "# Test configuration") - assert.NotContains(t, config, "commands:") + config := GenerateTestConfig(TypeUnknown) + require.True(t, config.OK) + text := config.Value.(string) + assert.Contains(t, text, "# Test configuration") + assert.NotContains(t, text, "commands:") } func TestConfig_GenerateTestConfig_Ugly_WailsUsesGoSuite(t *testing.T) { - config, err := GenerateTestConfig(TypeWails) - require.NoError(t, err) - assert.Contains(t, config, "go test ./...") - assert.Contains(t, config, "go test -race ./...") + config := GenerateTestConfig(TypeWails) + require.True(t, config.OK) + text := config.Value.(string) + assert.Contains(t, text, "go test ./...") + assert.Contains(t, text, "go test -race ./...") } func TestConfig_ParseGitRemote_Good_CommonFormats(t *testing.T) { @@ -94,22 +98,22 @@ func TestConfig_RenderConfig_Good_SingleSection(t *testing.T) { sections := []configSection{ {Key: "project", Values: []configValue{{Key: "name", Value: "test"}}}, } - result, err := renderConfig("Test", sections) - assert.NoError(t, err) - assert.Contains(t, result, "name: test") + result := renderConfig("Test", sections) + require.True(t, result.OK) + assert.Contains(t, result.Value.(string), "name: test") } func TestConfig_RenderConfig_Bad_UnsupportedValue(t *testing.T) { sections := []configSection{ {Key: "project", Values: []configValue{{Key: "name", Value: func() {}}}}, } - result, err := renderConfig("Test", sections) - assert.Error(t, err) - assert.Empty(t, result) + result := renderConfig("Test", sections) + assert.False(t, result.OK) + assert.Error(t, result.Value.(error)) } func TestConfig_RenderConfig_Ugly_EmptySections(t *testing.T) { - result, err := renderConfig("", nil) - assert.NoError(t, err) - assert.Equal(t, "", result) + result := renderConfig("", nil) + require.True(t, result.OK) + assert.Equal(t, "", result.Value.(string)) } diff --git a/pkg/setup/setup.go b/pkg/setup/setup.go index b920c38..c259030 100644 --- a/pkg/setup/setup.go +++ b/pkg/setup/setup.go @@ -9,7 +9,8 @@ import ( // Options controls one setup run. // -// err := svc.Run(setup.Options{Path: ".", Template: "auto", Force: true}) +// r := svc.Run(setup.Options{Path: ".", Template: "auto", Force: true}) +// if !r.OK { core.Print(nil, "%v", r.Value) } type Options struct { Path string // Target directory (default: cwd) DryRun bool // Preview only, don't write @@ -19,8 +20,9 @@ type Options struct { // Run generates `.core/` files and optional workspace scaffolding for a repo. // -// err := svc.Run(setup.Options{Path: ".", Template: "auto"}) -func (s *Service) Run(opts Options) error { +// r := svc.Run(setup.Options{Path: ".", Template: "auto"}) +// core.Println(r.OK) +func (s *Service) Run(opts Options) core.Result { if opts.Path == "" { opts.Path = core.Env("DIR_CWD") } @@ -37,19 +39,22 @@ func (s *Service) Run(opts Options) error { var tmplName string if opts.Template != "" { - var err error - tmplName, err = resolveTemplateName(opts.Template, projType) - if err != nil { - return err + templateResult := resolveTemplateName(opts.Template, projType) + if !templateResult.OK { + return templateResult } + tmplName = templateResult.Value.(string) if !templateExists(tmplName) { - return core.E("setup.Run", core.Concat("template not found: ", tmplName), nil) + return core.Result{ + Value: core.E("setup.Run", core.Concat("template not found: ", tmplName), nil), + OK: false, + } } } // Generate .core/ config files - if err := setupCoreDir(opts, projType); err != nil { - return err + if result := setupCoreDir(opts, projType); !result.OK { + return result } // Scaffold from dir template if requested @@ -57,11 +62,11 @@ func (s *Service) Run(opts Options) error { return s.scaffoldTemplate(opts, projType, tmplName) } - return nil + return core.Result{Value: opts.Path, OK: true} } // setupCoreDir creates .core/ with build.yaml and test.yaml. -func setupCoreDir(opts Options, projType ProjectType) error { +func setupCoreDir(opts Options, projType ProjectType) core.Result { coreDir := core.JoinPath(opts.Path, ".core") if opts.DryRun { @@ -70,33 +75,44 @@ func setupCoreDir(opts Options, projType ProjectType) error { } else { if r := fs.EnsureDir(coreDir); !r.OK { err, _ := r.Value.(error) - return core.E("setup.setupCoreDir", "create .core directory", err) + return core.Result{ + Value: core.E("setup.setupCoreDir", "create .core directory", err), + OK: false, + } } } // build.yaml - buildConfig, err := GenerateBuildConfig(opts.Path, projType) - if err != nil { - return core.E("setup.setupCoreDir", "generate build config", err) + buildConfig := GenerateBuildConfig(opts.Path, projType) + if !buildConfig.OK { + err, _ := buildConfig.Value.(error) + return core.Result{ + Value: core.E("setup.setupCoreDir", "generate build config", err), + OK: false, + } } - if err := writeConfig(core.JoinPath(coreDir, "build.yaml"), buildConfig, opts); err != nil { - return err + if result := writeConfig(core.JoinPath(coreDir, "build.yaml"), buildConfig.Value.(string), opts); !result.OK { + return result } // test.yaml - testConfig, err := GenerateTestConfig(projType) - if err != nil { - return core.E("setup.setupCoreDir", "generate test config", err) + testConfig := GenerateTestConfig(projType) + if !testConfig.OK { + err, _ := testConfig.Value.(error) + return core.Result{ + Value: core.E("setup.setupCoreDir", "generate test config", err), + OK: false, + } } - if err := writeConfig(core.JoinPath(coreDir, "test.yaml"), testConfig, opts); err != nil { - return err + if result := writeConfig(core.JoinPath(coreDir, "test.yaml"), testConfig.Value.(string), opts); !result.OK { + return result } - return nil + return core.Result{Value: coreDir, OK: true} } // scaffoldTemplate extracts a dir template into the target path. -func (s *Service) scaffoldTemplate(opts Options, projType ProjectType, tmplName string) error { +func (s *Service) scaffoldTemplate(opts Options, projType ProjectType, tmplName string) core.Result { core.Print(nil, "Template: %s", tmplName) data := &lib.WorkspaceData{ @@ -115,53 +131,62 @@ func (s *Service) scaffoldTemplate(opts Options, projType ProjectType, tmplName if opts.DryRun { core.Print(nil, "Would extract workspace/%s to %s", tmplName, opts.Path) core.Print(nil, " Template found: %s", tmplName) - return nil + return core.Result{Value: opts.Path, OK: true} } if err := lib.ExtractWorkspace(tmplName, opts.Path, data); err != nil { - return core.E("setup.scaffoldTemplate", core.Concat("extract workspace template ", tmplName), err) + return core.Result{ + Value: core.E("setup.scaffoldTemplate", core.Concat("extract workspace template ", tmplName), err), + OK: false, + } } - return nil + return core.Result{Value: opts.Path, OK: true} } -func writeConfig(path, content string, opts Options) error { +func writeConfig(path, content string, opts Options) core.Result { if opts.DryRun { core.Print(nil, " %s", path) - return nil + return core.Result{Value: path, OK: true} } if !opts.Force && fs.Exists(path) { core.Print(nil, " skip %s (exists, use --force to overwrite)", core.PathBase(path)) - return nil + return core.Result{Value: path, OK: true} } if r := fs.WriteMode(path, content, 0644); !r.OK { err, _ := r.Value.(error) - return core.E("setup.writeConfig", core.Concat("write ", core.PathBase(path)), err) + return core.Result{ + Value: core.E("setup.writeConfig", core.Concat("write ", core.PathBase(path)), err), + OK: false, + } } core.Print(nil, " created %s", path) - return nil + return core.Result{Value: path, OK: true} } -func resolveTemplateName(name string, projType ProjectType) (string, error) { +func resolveTemplateName(name string, projType ProjectType) core.Result { if name == "" { - return "", core.E("setup.resolveTemplateName", "template is required", nil) + return core.Result{ + Value: core.E("setup.resolveTemplateName", "template is required", nil), + OK: false, + } } if name == "auto" { switch projType { case TypeGo, TypeWails, TypePHP, TypeNode, TypeUnknown: - return "default", nil + return core.Result{Value: "default", OK: true} } } switch name { case "agent", "go", "php", "gui": - return "default", nil + return core.Result{Value: "default", OK: true} case "verify", "conventions": - return "review", nil + return core.Result{Value: "review", OK: true} default: - return name, nil + return core.Result{Value: name, OK: true} } } diff --git a/pkg/setup/setup_test.go b/pkg/setup/setup_test.go index 1e469e3..225110b 100644 --- a/pkg/setup/setup_test.go +++ b/pkg/setup/setup_test.go @@ -19,8 +19,8 @@ func TestSetup_Run_Good_WritesCoreConfigs(t *testing.T) { dir := t.TempDir() require.True(t, fs.WriteMode(core.JoinPath(dir, "go.mod"), "module example.com/test\n", 0644).OK) - err := newSetupService().Run(Options{Path: dir}) - require.NoError(t, err) + result := newSetupService().Run(Options{Path: dir}) + require.True(t, result.OK) build := fs.Read(core.JoinPath(dir, ".core", "build.yaml")) require.True(t, build.OK) @@ -35,8 +35,8 @@ func TestSetup_Run_Good_TemplateAlias(t *testing.T) { dir := t.TempDir() require.True(t, fs.WriteMode(core.JoinPath(dir, "go.mod"), "module example.com/test\n", 0644).OK) - err := newSetupService().Run(Options{Path: dir, Template: "agent"}) - require.NoError(t, err) + result := newSetupService().Run(Options{Path: dir, Template: "agent"}) + require.True(t, result.OK) prompt := fs.Read(core.JoinPath(dir, "PROMPT.md")) require.True(t, prompt.OK) @@ -47,8 +47,9 @@ func TestSetup_Run_Bad_MissingTemplateDoesNotWrite(t *testing.T) { dir := t.TempDir() require.True(t, fs.WriteMode(core.JoinPath(dir, "go.mod"), "module example.com/test\n", 0644).OK) - err := newSetupService().Run(Options{Path: dir, Template: "missing-template"}) - require.Error(t, err) + result := newSetupService().Run(Options{Path: dir, Template: "missing-template"}) + require.False(t, result.OK) + require.Error(t, result.Value.(error)) assert.False(t, fs.Exists(core.JoinPath(dir, ".core"))) } @@ -56,27 +57,28 @@ func TestSetup_Run_Ugly_DryRunDoesNotWrite(t *testing.T) { dir := t.TempDir() require.True(t, fs.WriteMode(core.JoinPath(dir, "go.mod"), "module example.com/test\n", 0644).OK) - err := newSetupService().Run(Options{Path: dir, Template: "agent", DryRun: true}) - require.NoError(t, err) + result := newSetupService().Run(Options{Path: dir, Template: "agent", DryRun: true}) + require.True(t, result.OK) assert.False(t, fs.Exists(core.JoinPath(dir, ".core"))) assert.False(t, fs.Exists(core.JoinPath(dir, "PROMPT.md"))) } func TestSetup_ResolveTemplateName_Good_Auto(t *testing.T) { - name, err := resolveTemplateName("auto", TypeGo) - require.NoError(t, err) - assert.Equal(t, "default", name) + name := resolveTemplateName("auto", TypeGo) + require.True(t, name.OK) + assert.Equal(t, "default", name.Value.(string)) } func TestSetup_ResolveTemplateName_Bad_Empty(t *testing.T) { - _, err := resolveTemplateName("", TypeGo) - require.Error(t, err) + result := resolveTemplateName("", TypeGo) + require.False(t, result.OK) + require.Error(t, result.Value.(error)) } func TestSetup_ResolveTemplateName_Ugly_ConventionsAlias(t *testing.T) { - name, err := resolveTemplateName("conventions", TypeGo) - require.NoError(t, err) - assert.Equal(t, "review", name) + name := resolveTemplateName("conventions", TypeGo) + require.True(t, name.OK) + assert.Equal(t, "review", name.Value.(string)) } func TestSetup_TemplateExists_Good_Default(t *testing.T) {