213 lines
6.4 KiB
Go
213 lines
6.4 KiB
Go
package legacyimport
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aoagents/agent-orchestrator/backend/internal/domain"
|
|
)
|
|
|
|
// mapPermission maps a legacy AgentPermissionMode to the rewrite PermissionMode
|
|
// (issue #247 §3). ok=false means "unset" (no permission to carry); lossy=true
|
|
// flags a remap that drops a distinction the rewrite cannot represent.
|
|
func mapPermission(legacy string) (mode domain.PermissionMode, ok, lossy bool) {
|
|
switch legacy {
|
|
case "":
|
|
return "", false, false
|
|
case "permissionless", "skip":
|
|
// legacy already collapses skip→permissionless, but a hand-edited config
|
|
// could carry the raw value, so map it explicitly.
|
|
return domain.PermissionModeBypassPermissions, true, false
|
|
case "auto-edit":
|
|
return domain.PermissionModeAcceptEdits, true, false
|
|
case "default":
|
|
return domain.PermissionModeDefault, true, false
|
|
case "suggest":
|
|
// The rewrite has no suggest/plan mode (#247 G8).
|
|
return domain.PermissionModeDefault, true, true
|
|
default:
|
|
return domain.PermissionModeDefault, true, true
|
|
}
|
|
}
|
|
|
|
// mapHarness maps a legacy agent plugin id to a rewrite harness, or ok=false
|
|
// when the rewrite has no such harness.
|
|
func mapHarness(agent string) (domain.AgentHarness, bool) {
|
|
if agent == "" {
|
|
return "", false
|
|
}
|
|
h := domain.AgentHarness(agent)
|
|
if h.IsKnown() {
|
|
return h, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func buildAgentConfig(src *legacyAgentConfig, notes *[]string, label string) domain.AgentConfig {
|
|
var out domain.AgentConfig
|
|
if src == nil {
|
|
return out
|
|
}
|
|
if m := strings.TrimSpace(src.Model); m != "" {
|
|
out.Model = m
|
|
}
|
|
if mode, ok, lossy := mapPermission(src.Permissions); ok {
|
|
out.Permissions = mode
|
|
if lossy {
|
|
*notes = append(*notes, fmt.Sprintf("%s permission %q mapped lossily to %q", label, src.Permissions, mode))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func buildRoleOverride(src *legacyRole, notes *[]string, label string) domain.RoleOverride {
|
|
var out domain.RoleOverride
|
|
if src == nil {
|
|
return out
|
|
}
|
|
if src.Agent != "" {
|
|
if h, ok := mapHarness(src.Agent); ok {
|
|
out.Harness = h
|
|
} else {
|
|
*notes = append(*notes, fmt.Sprintf("%s agent %q has no rewrite harness — dropped", label, src.Agent))
|
|
}
|
|
}
|
|
out.AgentConfig = buildAgentConfig(src.AgentConfig, notes, label+" agent")
|
|
return out
|
|
}
|
|
|
|
// buildProjectConfig maps a legacy project block to the typed rewrite config
|
|
// blob (issue #247 §3). It appends lossy/dropped notes and returns a config that
|
|
// may be IsZero (the store persists SQL NULL for that).
|
|
func buildProjectConfig(pc legacyProjectConfig, notes *[]string) domain.ProjectConfig {
|
|
var cfg domain.ProjectConfig
|
|
|
|
// defaultBranch: omit "main" so the common case keeps config NULL.
|
|
if b := strings.TrimSpace(pc.DefaultBranch); b != "" && b != domain.DefaultBranchName {
|
|
cfg.DefaultBranch = b
|
|
}
|
|
if pc.SessionPrefix != "" {
|
|
cfg.SessionPrefix = pc.SessionPrefix
|
|
}
|
|
if len(pc.Env) > 0 {
|
|
cfg.Env = make(map[string]string, len(pc.Env))
|
|
for k, v := range pc.Env {
|
|
cfg.Env[k] = v
|
|
}
|
|
}
|
|
if len(pc.Symlinks) > 0 {
|
|
cfg.Symlinks = append([]string(nil), pc.Symlinks...)
|
|
}
|
|
if len(pc.PostCreate) > 0 {
|
|
cfg.PostCreate = append([]string(nil), pc.PostCreate...)
|
|
}
|
|
cfg.AgentConfig = buildAgentConfig(pc.AgentConfig, notes, "agentConfig")
|
|
cfg.Worker = buildRoleOverride(pc.Worker, notes, "worker")
|
|
cfg.Orchestrator = buildRoleOverride(pc.Orchestrator, notes, "orchestrator")
|
|
|
|
// Surface project-level fields the rewrite has no home for (#247 §4).
|
|
var dropped []string
|
|
if pc.Tracker != nil {
|
|
dropped = append(dropped, "tracker")
|
|
}
|
|
if pc.SCM != nil {
|
|
dropped = append(dropped, "scm")
|
|
}
|
|
if pc.AgentRules != nil || pc.AgentRulesFile != nil || pc.OrchestratorRule != nil {
|
|
dropped = append(dropped, "rules")
|
|
}
|
|
if pc.Runtime != nil {
|
|
dropped = append(dropped, "runtime")
|
|
}
|
|
if pc.Workspace != nil {
|
|
dropped = append(dropped, "workspace")
|
|
}
|
|
if pc.Reactions != nil {
|
|
dropped = append(dropped, "reactions")
|
|
}
|
|
if len(dropped) > 0 {
|
|
*notes = append(*notes, "dropped project fields with no rewrite home: "+strings.Join(dropped, ", "))
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// projectRowDeps are the host effects the project mapper needs: git origin
|
|
// resolution and the fallback "now" timestamp. Injected so the mapper is pure
|
|
// and unit-testable.
|
|
type projectRowDeps struct {
|
|
repoOriginURL func(path string) string
|
|
configMtime string // ISO timestamp of config.yaml, or "" if unknown
|
|
now time.Time
|
|
}
|
|
|
|
// buildProjectRecord builds the rewrite projects row for one legacy project
|
|
// (issue #247 §1). The rewrite no longer fills server-side fields, so the
|
|
// importer computes repo_origin_url, registered_at, kind, display_name, config.
|
|
func buildProjectRecord(id string, pc legacyProjectConfig, prefs preferences, reg registeredManifest, deps projectRowDeps) (domain.ProjectRecord, []string) {
|
|
var notes []string
|
|
cfg := buildProjectConfig(pc, ¬es)
|
|
|
|
path := normalizePath(pc.Path)
|
|
|
|
// display_name: preferences.displayName → config name → "" (rewrite falls
|
|
// back to id on read, so only persist a real, non-id name).
|
|
displayName := ""
|
|
if p, ok := prefs.Projects[id]; ok && p.DisplayName != "" {
|
|
displayName = p.DisplayName
|
|
} else if pc.Name != "" && pc.Name != id {
|
|
displayName = pc.Name
|
|
}
|
|
|
|
// registered_at: registered.json addedAt → config mtime → import time.
|
|
registeredAt := deps.now
|
|
if iso := reg.addedAt(id, pc.Path); iso != "" {
|
|
if t, err := time.Parse(time.RFC3339, iso); err == nil {
|
|
registeredAt = t
|
|
}
|
|
} else if deps.configMtime != "" {
|
|
if t, err := time.Parse(time.RFC3339, deps.configMtime); err == nil {
|
|
registeredAt = t
|
|
}
|
|
}
|
|
|
|
origin := ""
|
|
if deps.repoOriginURL != nil {
|
|
origin = deps.repoOriginURL(path)
|
|
}
|
|
|
|
return domain.ProjectRecord{
|
|
ID: id,
|
|
Path: path,
|
|
RepoOriginURL: origin,
|
|
DisplayName: displayName,
|
|
RegisteredAt: registeredAt,
|
|
Kind: domain.ProjectKindSingleRepo,
|
|
Config: cfg,
|
|
}, notes
|
|
}
|
|
|
|
// normalizePath ~-expands then absolutises+cleans a legacy project path, matching
|
|
// the rewrite's normalizePath. A path that cannot be absolutised is returned
|
|
// cleaned but relative (best effort; the rewrite re-derives worktrees anyway).
|
|
func normalizePath(p string) string {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" {
|
|
return ""
|
|
}
|
|
if p == "~" || strings.HasPrefix(p, "~/") {
|
|
if home, err := userHomeDir(); err == nil {
|
|
if p == "~" {
|
|
p = home
|
|
} else {
|
|
p = filepath.Join(home, p[2:])
|
|
}
|
|
}
|
|
}
|
|
if abs, err := filepath.Abs(p); err == nil {
|
|
return abs
|
|
}
|
|
return filepath.Clean(p)
|
|
}
|