L0b (factory bootstrap): - bootstrap.ps1 (Windows): pre-checks, interactive config, clone .factory, home.toml - bootstrap.sh (Linux/Mac): same functionality for bash - Idempotent re-run with home.toml verification L1 (Go CLI): - factory.yaml: manifest schema with components, deps, health-checks - cmd/factory: Cobra CLI (install/update/status/diagnose) - pkg/config: home.toml reader - pkg/manifest: factory.yaml parser + topological sort - pkg/health: health-check logic (file/dir/mcp_tool/command) Scripts: - build.sh: cross-compile for linux/win/darwin × amd64/arm64 - build.ps1: PowerShell cross-compile Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
124 lines
2.5 KiB
Go
124 lines
2.5 KiB
Go
package manifest
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/goccy/go-yaml"
|
|
)
|
|
|
|
type GitSource struct {
|
|
Type string `yaml:"type"`
|
|
URL string `yaml:"url"`
|
|
Branch string `yaml:"branch"`
|
|
}
|
|
|
|
type PostInstall struct {
|
|
Shell string `yaml:"shell"`
|
|
Script string `yaml:"script"`
|
|
ShellAlt string `yaml:"shell_alt"`
|
|
ScriptAlt string `yaml:"script_alt"`
|
|
}
|
|
|
|
type HealthCheck struct {
|
|
Type string `yaml:"type"`
|
|
Path string `yaml:"path"`
|
|
Tool string `yaml:"tool"`
|
|
Command string `yaml:"command"`
|
|
}
|
|
|
|
type Component struct {
|
|
Name string `yaml:"name"`
|
|
DisplayName string `yaml:"display_name"`
|
|
Source *GitSource `yaml:"source"`
|
|
Target string `yaml:"target"`
|
|
Deps []string `yaml:"deps"`
|
|
SetupSkill string `yaml:"setup_skill"`
|
|
PostInstall *PostInstall `yaml:"post_install"`
|
|
HealthCheck *HealthCheck `yaml:"health_check"`
|
|
Optional bool `yaml:"optional"`
|
|
Clients []string `yaml:"clients"`
|
|
}
|
|
|
|
type Manifest struct {
|
|
Version string `yaml:"version"`
|
|
GitHost string `yaml:"git_host"`
|
|
GitOrg string `yaml:"git_org"`
|
|
Components []Component `yaml:"components"`
|
|
}
|
|
|
|
func Load(path string, vars map[string]string) (*Manifest, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read manifest: %w", err)
|
|
}
|
|
|
|
// Substitute variables
|
|
content := string(data)
|
|
for k, v := range vars {
|
|
content = strings.ReplaceAll(content, "{"+k+"}", v)
|
|
}
|
|
|
|
var m Manifest
|
|
if err := yaml.Unmarshal([]byte(content), &m); err != nil {
|
|
return nil, fmt.Errorf("failed to parse manifest: %w", err)
|
|
}
|
|
|
|
// Set defaults
|
|
if m.GitHost == "" {
|
|
m.GitHost = "https://github.com"
|
|
}
|
|
|
|
return &m, nil
|
|
}
|
|
|
|
func (m *Manifest) GetComponent(name string) (*Component, bool) {
|
|
for i := range m.Components {
|
|
if m.Components[i].Name == name {
|
|
return &m.Components[i], true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (m *Manifest) GetDependencyOrder() []string {
|
|
// Topological sort for component installation order
|
|
visited := make(map[string]bool)
|
|
order := []string{}
|
|
|
|
var visit func(string)
|
|
visit = func(name string) {
|
|
if visited[name] {
|
|
return
|
|
}
|
|
visited[name] = true
|
|
|
|
c, ok := m.GetComponent(name)
|
|
if ok {
|
|
for _, dep := range c.Deps {
|
|
visit(dep)
|
|
}
|
|
}
|
|
order = append(order, name)
|
|
}
|
|
|
|
for _, c := range m.Components {
|
|
visit(c.Name)
|
|
}
|
|
|
|
return order
|
|
}
|
|
|
|
func (c *Component) IsApplicable(client string) bool {
|
|
if len(c.Clients) == 0 {
|
|
return true
|
|
}
|
|
for _, cl := range c.Clients {
|
|
if cl == client {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|