agent/pkg/setup/config.go
Snider 537226bd4d feat: AX v0.8.0 upgrade — Core features + quality gates
AX Quality Gates (RFC-025):
- Eliminate os/exec from all test + production code (12+ files)
- Eliminate encoding/json from all test files (15 files, 66 occurrences)
- Eliminate os from all test files except TestMain (Go runtime contract)
- Eliminate path/filepath, net/url from all files
- String concat: 39 violations replaced with core.Concat()
- Test naming AX-7: 264 test functions renamed across all 6 packages
- Example test 1:1 coverage complete

Core Features Adopted:
- Task Composition: agent.completion pipeline (QA → PR → Verify → Ingest → Poke)
- PerformAsync: completion pipeline runs with WaitGroup + progress tracking
- Config: agents.yaml loaded once, feature flags (auto-qa/pr/merge/ingest)
- Named Locks: c.Lock("drain") for queue serialisation
- Registry: workspace state with cross-package QUERY access
- QUERY: c.QUERY(WorkspaceQuery{Status: "running"}) for cross-service queries
- Action descriptions: 25+ Actions self-documenting
- Data mounts: prompts/tasks/flows/personas/workspaces via c.Data()
- Content Actions: agentic.prompt/task/flow/persona callable via IPC
- Drive endpoints: forge + brain registered with tokens
- Drive REST helpers: DriveGet/DrivePost/DriveDo for Drive-aware HTTP
- HandleIPCEvents: auto-discovered by WithService (no manual wiring)
- Entitlement: frozen-queue gate on write Actions
- CLI dispatch: workspace dispatch wired to real dispatch method
- CLI: --quiet/-q and --debug/-d global flags
- CLI: banner, version, check (with service/action/command counts), env
- main.go: minimal — 5 services + c.Run(), no os import
- cmd tests: 84.2% coverage (was 0%)

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 06:38:02 +00:00

203 lines
4.4 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package setup
import (
core "dappco.re/go/core"
"gopkg.in/yaml.v3"
)
// ConfigData holds the data passed to config templates.
type ConfigData struct {
Name string
Description string
Type string
Module string
Repository string
GoVersion string
Targets []Target
Commands []Command
Env map[string]string
}
// Target is a build target (os/arch pair).
type Target struct {
OS string
Arch string
}
// Command is a named runnable command.
type Command struct {
Name string
Run string
}
type configSection struct {
Key string
Values []configValue
}
type configValue struct {
Key string
Value any
}
// GenerateBuildConfig renders a build.yaml for the detected project type.
//
// content, err := setup.GenerateBuildConfig("/repo", setup.TypeGo)
func GenerateBuildConfig(path string, projType ProjectType) (string, error) {
name := core.PathBase(path)
sections := []configSection{
{
Key: "project",
Values: []configValue{
{Key: "name", Value: name},
{Key: "type", Value: string(projType)},
},
},
}
switch projType {
case TypeGo, TypeWails:
sections = append(sections, configSection{
Key: "build",
Values: []configValue{
{Key: "main", Value: core.Concat("./cmd/", name)},
{Key: "binary", Value: name},
{Key: "cgo", Value: false},
},
})
case TypePHP:
sections = append(sections, configSection{
Key: "build",
Values: []configValue{
{Key: "dockerfile", Value: "Dockerfile"},
{Key: "image", Value: name},
},
})
case TypeNode:
sections = append(sections, configSection{
Key: "build",
Values: []configValue{
{Key: "script", Value: "npm run build"},
{Key: "output", Value: "dist"},
},
})
}
return renderConfig(core.Concat(name, " build configuration"), sections)
}
// GenerateTestConfig renders a test.yaml for the detected project type.
//
// content, err := setup.GenerateTestConfig(setup.TypeGo)
func GenerateTestConfig(projType ProjectType) (string, error) {
var sections []configSection
switch projType {
case TypeGo, TypeWails:
sections = []configSection{
{
Key: "commands",
Values: []configValue{
{Key: "unit", Value: "go test ./..."},
{Key: "coverage", Value: "go test -coverprofile=coverage.out ./..."},
{Key: "race", Value: "go test -race ./..."},
},
},
}
case TypePHP:
sections = []configSection{
{
Key: "commands",
Values: []configValue{
{Key: "unit", Value: "vendor/bin/pest --parallel"},
{Key: "lint", Value: "vendor/bin/pint --test"},
},
},
}
case TypeNode:
sections = []configSection{
{
Key: "commands",
Values: []configValue{
{Key: "unit", Value: "npm test"},
{Key: "lint", Value: "npm run lint"},
},
},
}
}
return renderConfig("Test configuration", sections)
}
func renderConfig(comment string, sections []configSection) (string, error) {
builder := core.NewBuilder()
if comment != "" {
builder.WriteString("# ")
builder.WriteString(comment)
builder.WriteString("\n\n")
}
for idx, section := range sections {
builder.WriteString(section.Key)
builder.WriteString(":\n")
for _, value := range section.Values {
scalar, err := yaml.Marshal(value.Value)
if err != nil {
return "", core.E("setup.renderConfig", core.Concat("marshal ", section.Key, ".", value.Key), err)
}
builder.WriteString(" ")
builder.WriteString(value.Key)
builder.WriteString(": ")
builder.WriteString(core.Trim(string(scalar)))
builder.WriteString("\n")
}
if idx < len(sections)-1 {
builder.WriteString("\n")
}
}
return builder.String(), nil
}
func parseGitRemote(remote string) string {
if remote == "" {
return ""
}
// HTTPS/HTTP URL — extract path after host
if core.Contains(remote, "://") {
parts := core.SplitN(remote, "://", 2)
if len(parts) == 2 {
rest := parts[1]
if idx := core.Split(rest, "/"); len(idx) > 1 {
// Skip host, take path
pathStart := len(idx[0]) + 1
if pathStart < len(rest) {
return trimRemotePath(rest[pathStart:])
}
}
}
}
parts := core.SplitN(remote, ":", 2)
if len(parts) == 2 && core.Contains(parts[0], "@") {
return trimRemotePath(parts[1])
}
if core.Contains(remote, "/") {
return trimRemotePath(remote)
}
return ""
}
func trimRemotePath(remote string) string {
trimmed := core.TrimPrefix(remote, "/")
return core.TrimSuffix(trimmed, ".git")
}