fix(setup): make setup APIs AX-native

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-29 23:01:10 +00:00
parent f8cd0ff11a
commit feaa4dec5e
5 changed files with 153 additions and 110 deletions

View file

@ -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 {

View file

@ -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

View file

@ -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))
}

View file

@ -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}
}
}

View file

@ -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) {