Files

232 lines
6.6 KiB
Go

package runtimecodex
import (
"context"
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"inbox/internal/app/runtimeconfig"
"inbox/internal/domain/role"
)
const (
WorkspaceDir = ".inbox/runtime-codex"
ContainerDir = "/workspace/.inbox/runtime-codex"
ContainerHome = "/root"
ContainerCodex = "/root/.codex"
configFilename = "config.toml"
authFilename = "auth.json"
)
type RoleCatalog interface {
ListRoles(ctx context.Context) ([]role.Definition, error)
}
type RoleResolver interface {
ResolveRole(ctx context.Context, workspaceID, roleName string) (runtimeconfig.ResolvedRole, error)
}
type Materializer struct {
roles RoleCatalog
resolver RoleResolver
}
func NewMaterializer(roles RoleCatalog, resolver RoleResolver) *Materializer {
return &Materializer{
roles: roles,
resolver: resolver,
}
}
func (m *Materializer) Sync(ctx context.Context, workspaceID, workspaceRoot string) error {
if m == nil {
return nil
}
if strings.TrimSpace(workspaceRoot) == "" {
return fmt.Errorf("workspace root is required")
}
items, err := m.roles.ListRoles(ctx)
if err != nil {
return fmt.Errorf("list roles for runtime codex config: %w", err)
}
baseDir := filepath.Join(workspaceRoot, ".inbox")
liveDir := filepath.Join(workspaceRoot, filepath.FromSlash(WorkspaceDir))
if err := os.MkdirAll(baseDir, 0755); err != nil {
return fmt.Errorf("ensure runtime codex base dir: %w", err)
}
tmpDir, err := os.MkdirTemp(baseDir, "runtime-codex-")
if err != nil {
return fmt.Errorf("create runtime codex temp dir: %w", err)
}
cleanup := func() { _ = os.RemoveAll(tmpDir) }
for _, item := range items {
item = role.NormalizeDefinition(item)
if !item.IsEnabled || item.ExecutorKind != role.ExecutorKindCodex {
continue
}
resolved, err := m.resolver.ResolveRole(ctx, workspaceID, item.Name)
if err != nil {
cleanup()
return fmt.Errorf("resolve role %q for runtime codex config: %w", item.Name, err)
}
if _, err := WriteResolvedRoleHome(tmpDir, resolved); err != nil {
cleanup()
return fmt.Errorf("write runtime codex home for %q: %w", item.Name, err)
}
if err := writeResolvedRoleSkills(tmpDir, resolved); err != nil {
cleanup()
return fmt.Errorf("write runtime codex skills for %q: %w", item.Name, err)
}
}
_ = os.RemoveAll(liveDir)
if err := os.Rename(tmpDir, liveDir); err != nil {
cleanup()
return fmt.Errorf("publish runtime codex dir: %w", err)
}
return nil
}
func ContainerHomeDir(roleName string) string {
return path.Join(ContainerDir, sanitizeRolePath(roleName))
}
func ContainerUserHomeDir() string {
return ContainerHome
}
func ContainerCodexDir() string {
return ContainerCodex
}
func WorkspaceHomeDir(workspaceRoot, roleName string) string {
return filepath.Join(workspaceRoot, filepath.FromSlash(WorkspaceDir), sanitizeRolePath(roleName))
}
func WorkspaceCodexDir(workspaceRoot, roleName string) string {
return filepath.Join(WorkspaceHomeDir(workspaceRoot, roleName), ".codex")
}
func WriteResolvedRoleHome(root string, resolved runtimeconfig.ResolvedRole) (string, error) {
roleHome := filepath.Join(root, sanitizeRolePath(resolved.Role.Name))
codexDir := filepath.Join(roleHome, ".codex")
if err := os.MkdirAll(codexDir, 0755); err != nil {
return "", fmt.Errorf("create runtime codex dir for %q: %w", resolved.Role.Name, err)
}
configText := normalizeConfigTOML(resolved.Config.ConfigTOML)
if err := os.WriteFile(filepath.Join(codexDir, configFilename), []byte(configText), 0644); err != nil {
return "", fmt.Errorf("write config.toml for %q: %w", resolved.Role.Name, err)
}
authBytes, err := normalizeAuthJSON(resolved.Config.AuthJSON)
if err != nil {
return "", fmt.Errorf("normalize auth.json for %q: %w", resolved.Role.Name, err)
}
if err := os.WriteFile(filepath.Join(codexDir, authFilename), authBytes, 0600); err != nil {
return "", fmt.Errorf("write auth.json for %q: %w", resolved.Role.Name, err)
}
return roleHome, nil
}
func HostContainerRuntimeRoot(projectRoot, workspaceID string) string {
return filepath.Join(projectRoot, ".runtime", "container-codex", sanitizeRolePath(workspaceID))
}
func HostContainerCodexDir(projectRoot, workspaceID, roleName string) string {
return WorkspaceCodexDir(HostContainerRuntimeRoot(projectRoot, workspaceID), roleName)
}
func HostLeaderRuntimeRoot(projectRoot, workspaceID string) string {
return filepath.Join(projectRoot, ".runtime", "leader-codex", sanitizeRolePath(workspaceID))
}
func HostLeaderHomeDir(projectRoot, workspaceID, roleName string) string {
return filepath.Join(HostLeaderRuntimeRoot(projectRoot, workspaceID), sanitizeRolePath(roleName))
}
func HostLeaderCodexDir(projectRoot, workspaceID, roleName string) string {
return filepath.Join(HostLeaderHomeDir(projectRoot, workspaceID, roleName), ".codex")
}
func sanitizeRolePath(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return "role"
}
replacer := strings.NewReplacer(
"/", "-",
"\\", "-",
":", "-",
"*", "-",
"?", "-",
"\"", "-",
"<", "-",
">", "-",
"|", "-",
" ", "-",
)
value = replacer.Replace(value)
value = strings.Trim(value, ".-")
if value == "" {
return "role"
}
return value
}
func normalizeConfigTOML(value string) string {
return strings.TrimSpace(value)
}
func normalizeAuthJSON(value string) ([]byte, error) {
value = strings.TrimSpace(value)
if value == "" {
value = "{}"
}
if !json.Valid([]byte(value)) {
return nil, fmt.Errorf("invalid json")
}
var decoded any
if err := json.Unmarshal([]byte(value), &decoded); err != nil {
return nil, err
}
return json.MarshalIndent(decoded, "", " ")
}
func writeResolvedRoleSkills(root string, resolved runtimeconfig.ResolvedRole) error {
if len(resolved.Skills) == 0 {
return nil
}
codexDir := filepath.Join(root, sanitizeRolePath(resolved.Role.Name), ".codex")
skillsRoot := filepath.Join(codexDir, "skills")
for _, item := range resolved.Skills {
skillKey := strings.TrimSpace(item.Skill.SkillKey)
if skillKey == "" {
skillKey = sanitizeRolePath(item.Skill.ID)
}
if skillKey == "" {
return fmt.Errorf("resolved skill key is required")
}
targetDir := filepath.Join(skillsRoot, sanitizeRolePath(skillKey))
if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("create skill dir %s: %w", targetDir, err)
}
skillBody := strings.TrimSpace(item.Skill.ContentMarkdown)
if skillBody == "" {
return fmt.Errorf("skill %q content_markdown is required", skillKey)
}
if err := os.WriteFile(filepath.Join(targetDir, "SKILL.md"), []byte(skillBody), 0644); err != nil {
return fmt.Errorf("write SKILL.md for %q: %w", skillKey, err)
}
}
return nil
}