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>
211 lines
5.5 KiB
Go
211 lines
5.5 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package setup
|
|
|
|
import (
|
|
"dappco.re/go/agent/pkg/lib"
|
|
core "dappco.re/go/core"
|
|
)
|
|
|
|
// Options controls setup behaviour.
|
|
//
|
|
// err := svc.Run(setup.Options{Path: ".", Force: true})
|
|
type Options struct {
|
|
Path string // Target directory (default: cwd)
|
|
DryRun bool // Preview only, don't write
|
|
Force bool // Overwrite existing files
|
|
Template string // Workspace template or compatibility alias (default, review, security, agent, go, php, gui, auto)
|
|
}
|
|
|
|
// Run performs the workspace setup at the given path.
|
|
// It detects the project type, generates .core/ configs,
|
|
// and optionally scaffolds a workspace from a dir template.
|
|
//
|
|
// svc.Run(setup.Options{Path: ".", Template: "auto"})
|
|
func (s *Service) Run(opts Options) error {
|
|
if opts.Path == "" {
|
|
opts.Path = core.Env("DIR_CWD")
|
|
}
|
|
opts.Path = absolutePath(opts.Path)
|
|
|
|
projType := Detect(opts.Path)
|
|
allTypes := DetectAll(opts.Path)
|
|
|
|
core.Print(nil, "Project: %s", core.PathBase(opts.Path))
|
|
core.Print(nil, "Type: %s", projType)
|
|
if len(allTypes) > 1 {
|
|
core.Print(nil, "Also: %v (polyglot)", allTypes)
|
|
}
|
|
|
|
// Generate .core/ config files
|
|
if err := setupCoreDir(opts, projType); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Scaffold from dir template if requested
|
|
if opts.Template != "" {
|
|
return s.scaffoldTemplate(opts, projType)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// setupCoreDir creates .core/ with build.yaml and test.yaml.
|
|
func setupCoreDir(opts Options, projType ProjectType) error {
|
|
coreDir := core.JoinPath(opts.Path, ".core")
|
|
|
|
if opts.DryRun {
|
|
core.Print(nil, "")
|
|
core.Print(nil, "Would create %s/", coreDir)
|
|
} else {
|
|
if r := fs.EnsureDir(coreDir); !r.OK {
|
|
err, _ := r.Value.(error)
|
|
return core.E("setup.setupCoreDir", "create .core directory", err)
|
|
}
|
|
}
|
|
|
|
// build.yaml
|
|
buildConfig, err := GenerateBuildConfig(opts.Path, projType)
|
|
if err != nil {
|
|
return core.E("setup.setupCoreDir", "generate build config", err)
|
|
}
|
|
if err := writeConfig(core.JoinPath(coreDir, "build.yaml"), buildConfig, opts); err != nil {
|
|
return err
|
|
}
|
|
|
|
// test.yaml
|
|
testConfig, err := GenerateTestConfig(projType)
|
|
if err != nil {
|
|
return core.E("setup.setupCoreDir", "generate test config", err)
|
|
}
|
|
if err := writeConfig(core.JoinPath(coreDir, "test.yaml"), testConfig, opts); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// scaffoldTemplate extracts a dir template into the target path.
|
|
func (s *Service) scaffoldTemplate(opts Options, projType ProjectType) error {
|
|
tmplName, err := resolveTemplateName(opts.Template, projType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
core.Print(nil, "Template: %s", tmplName)
|
|
|
|
data := &lib.WorkspaceData{
|
|
Repo: core.PathBase(opts.Path),
|
|
Branch: "main",
|
|
Task: core.Sprintf("Initialise %s project tooling.", projType),
|
|
Agent: "setup",
|
|
Language: string(projType),
|
|
Prompt: "This workspace was scaffolded by pkg/setup. Review the repository and continue from the generated context files.",
|
|
Flow: formatFlow(projType),
|
|
RepoDescription: s.DetectGitRemote(opts.Path),
|
|
BuildCmd: defaultBuildCommand(projType),
|
|
TestCmd: defaultTestCommand(projType),
|
|
}
|
|
|
|
if !templateExists(tmplName) {
|
|
return core.E("setup.scaffoldTemplate", core.Concat("template not found: ", tmplName), nil)
|
|
}
|
|
|
|
if opts.DryRun {
|
|
core.Print(nil, "Would extract workspace/%s to %s", tmplName, opts.Path)
|
|
core.Print(nil, " Template found: %s", tmplName)
|
|
return nil
|
|
}
|
|
|
|
if err := lib.ExtractWorkspace(tmplName, opts.Path, data); err != nil {
|
|
return core.E("setup.scaffoldTemplate", core.Concat("extract workspace template ", tmplName), err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeConfig(path, content string, opts Options) error {
|
|
if opts.DryRun {
|
|
core.Print(nil, " %s", path)
|
|
return nil
|
|
}
|
|
|
|
if !opts.Force && fs.Exists(path) {
|
|
core.Print(nil, " skip %s (exists, use --force to overwrite)", core.PathBase(path))
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
}
|
|
core.Print(nil, " created %s", path)
|
|
return nil
|
|
}
|
|
|
|
func resolveTemplateName(name string, projType ProjectType) (string, error) {
|
|
if name == "" {
|
|
return "", core.E("setup.resolveTemplateName", "template is required", nil)
|
|
}
|
|
|
|
if name == "auto" {
|
|
switch projType {
|
|
case TypeGo, TypeWails, TypePHP, TypeNode, TypeUnknown:
|
|
return "default", nil
|
|
}
|
|
}
|
|
|
|
switch name {
|
|
case "agent", "go", "php", "gui":
|
|
return "default", nil
|
|
case "verify", "conventions":
|
|
return "review", nil
|
|
default:
|
|
return name, nil
|
|
}
|
|
}
|
|
|
|
func templateExists(name string) bool {
|
|
for _, tmpl := range lib.ListWorkspaces() {
|
|
if tmpl == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func defaultBuildCommand(projType ProjectType) string {
|
|
switch projType {
|
|
case TypeGo, TypeWails:
|
|
return "go build ./..."
|
|
case TypePHP:
|
|
return "composer test"
|
|
case TypeNode:
|
|
return "npm run build"
|
|
default:
|
|
return "make build"
|
|
}
|
|
}
|
|
|
|
func defaultTestCommand(projType ProjectType) string {
|
|
switch projType {
|
|
case TypeGo, TypeWails:
|
|
return "go test ./..."
|
|
case TypePHP:
|
|
return "composer test"
|
|
case TypeNode:
|
|
return "npm test"
|
|
default:
|
|
return "make test"
|
|
}
|
|
}
|
|
|
|
func formatFlow(projType ProjectType) string {
|
|
builder := core.NewBuilder()
|
|
builder.WriteString("- Build: `")
|
|
builder.WriteString(defaultBuildCommand(projType))
|
|
builder.WriteString("`\n")
|
|
builder.WriteString("- Test: `")
|
|
builder.WriteString(defaultTestCommand(projType))
|
|
builder.WriteString("`")
|
|
return builder.String()
|
|
}
|