feat: L0b bootstrap scripts + L1 Go CLI structure
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>
This commit is contained in:
322
cmd/factory/main.go
Normal file
322
cmd/factory/main.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/OpeItcLoc03/factory/pkg/config"
|
||||
"github.com/OpeItcLoc03/factory/pkg/health"
|
||||
"github.com/OpeItcLoc03/factory/pkg/manifest"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
verbose bool
|
||||
dryRun bool
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "factory",
|
||||
Short: "Software factory installer/updater",
|
||||
Long: `Factory — L1 orchestrator for software development infrastructure.`,
|
||||
}
|
||||
|
||||
var installCmd = &cobra.Command{
|
||||
Use: "install [component...]",
|
||||
Short: "Install factory components",
|
||||
Run: runInstall,
|
||||
}
|
||||
|
||||
var updateCmd = &cobra.Command{
|
||||
Use: "update [component...]",
|
||||
Short: "Update installed components",
|
||||
Run: runUpdate,
|
||||
}
|
||||
|
||||
var statusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Show component health status",
|
||||
Run: runStatus,
|
||||
}
|
||||
|
||||
var diagnoseCmd = &cobra.Command{
|
||||
Use: "diagnose [component]",
|
||||
Short: "Run diagnostics on a component",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: runDiagnose,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
||||
rootCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "show what would be done without doing it")
|
||||
|
||||
rootCmd.AddCommand(installCmd)
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
rootCmd.AddCommand(statusCmd)
|
||||
rootCmd.AddCommand(diagnoseCmd)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func loadManifest() (*manifest.Manifest, *config.HomeConfig, error) {
|
||||
home, err := config.LoadHome()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
vars := map[string]string{
|
||||
"gitea": "", // from manifest
|
||||
"projects_dir": home.ProjectsDir,
|
||||
"factory": home.GetFactoryDir(),
|
||||
"home": os.Getenv("HOME"),
|
||||
"git_host": "",
|
||||
"git_org": "",
|
||||
}
|
||||
|
||||
m, err := manifest.Load(home.GetManifestPath(), vars)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Add git host/org from manifest
|
||||
vars["git_host"] = m.GitHost
|
||||
vars["git_org"] = m.GitOrg
|
||||
|
||||
return m, home, nil
|
||||
}
|
||||
|
||||
func runInstall(cmd *cobra.Command, args []string) {
|
||||
m, home, err := loadManifest()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Determine components to install
|
||||
toInstall := []string{}
|
||||
if len(args) == 0 {
|
||||
// Install all in dependency order
|
||||
toInstall = m.GetDependencyOrder()
|
||||
} else {
|
||||
toInstall = args
|
||||
}
|
||||
|
||||
// Filter by client
|
||||
components := filterByClient(m, toInstall, home.Client)
|
||||
|
||||
fmt.Printf("Installing %d components for client '%s'...\n", len(components), home.Client)
|
||||
|
||||
for _, name := range components {
|
||||
comp, ok := m.GetComponent(name)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "Unknown component: %s\n", name)
|
||||
continue
|
||||
}
|
||||
|
||||
if comp.Optional && !contains(args, name) {
|
||||
continue
|
||||
}
|
||||
|
||||
installComponent(comp, home)
|
||||
}
|
||||
}
|
||||
|
||||
func runUpdate(cmd *cobra.Command, args []string) {
|
||||
m, home, err := loadManifest()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
toUpdate := []string{}
|
||||
if len(args) == 0 {
|
||||
toUpdate = m.GetDependencyOrder()
|
||||
} else {
|
||||
toUpdate = args
|
||||
}
|
||||
|
||||
components := filterByClient(m, toUpdate, home.Client)
|
||||
|
||||
fmt.Printf("Updating %d components...\n", len(components))
|
||||
|
||||
for _, name := range components {
|
||||
comp, ok := m.GetComponent(name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
updateComponent(comp, home)
|
||||
}
|
||||
}
|
||||
|
||||
func runStatus(cmd *cobra.Command, args []string) {
|
||||
m, home, err := loadManifest()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Health check variables
|
||||
vars := map[string]string{
|
||||
"projects_dir": home.ProjectsDir,
|
||||
"factory": home.GetFactoryDir(),
|
||||
"home": os.Getenv("HOME"),
|
||||
}
|
||||
|
||||
// Table header
|
||||
fmt.Printf("\n%-25s %-10s %s\n", "Component", "Status", "Message")
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
|
||||
// Sort by name
|
||||
names := []string{}
|
||||
for _, c := range m.Components {
|
||||
if c.IsApplicable(home.Client) {
|
||||
names = append(names, c.Name)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
for _, name := range names {
|
||||
comp, _ := m.GetComponent(name)
|
||||
result, _ := health.Check(comp, vars)
|
||||
|
||||
status := "✓"
|
||||
if !result.Healthy {
|
||||
status = "✗"
|
||||
}
|
||||
|
||||
display := comp.Name
|
||||
if comp.DisplayName != "" {
|
||||
display = comp.DisplayName
|
||||
}
|
||||
|
||||
msg := result.Message
|
||||
if msg == "" {
|
||||
msg = "OK"
|
||||
}
|
||||
|
||||
fmt.Printf("%-25s %-10s %s\n", display, status, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func runDiagnose(cmd *cobra.Command, args []string) {
|
||||
name := args[0]
|
||||
m, home, err := loadManifest()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
comp, ok := m.GetComponent(name)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "Unknown component: %s\n", name)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
vars := map[string]string{
|
||||
"projects_dir": home.ProjectsDir,
|
||||
"factory": home.GetFactoryDir(),
|
||||
"home": os.Getenv("HOME"),
|
||||
}
|
||||
|
||||
result, err := health.Check(comp, vars)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error checking %s: %v\n", name, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Component: %s\n", comp.Name)
|
||||
fmt.Printf("Healthy: %v\n", result.Healthy)
|
||||
if result.Message != "" {
|
||||
fmt.Printf("Message: %s\n", result.Message)
|
||||
}
|
||||
|
||||
// Print component details
|
||||
fmt.Printf("\nDetails:\n")
|
||||
fmt.Printf(" Target: %s\n", comp.Target)
|
||||
if comp.Source != nil {
|
||||
fmt.Printf(" Source: %s\n", comp.Source.URL)
|
||||
}
|
||||
if len(comp.Deps) > 0 {
|
||||
fmt.Printf(" Dependencies: %s\n", strings.Join(comp.Deps, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
func installComponent(comp *manifest.Component, home *config.HomeConfig) {
|
||||
fmt.Printf("Installing %s...\n", comp.Name)
|
||||
|
||||
if dryRun {
|
||||
fmt.Printf(" [dry-run] Would install %s\n", comp.Name)
|
||||
return
|
||||
}
|
||||
|
||||
// Clone or update
|
||||
if comp.Source != nil {
|
||||
cloneOrUpdate(comp, home)
|
||||
}
|
||||
|
||||
// Run post-install
|
||||
if comp.PostInstall != nil {
|
||||
runPostInstall(comp, home)
|
||||
}
|
||||
|
||||
// Print setup skill instruction
|
||||
if comp.SetupSkill != "" {
|
||||
fmt.Printf("\n Next: run /%s in Claude\n", comp.SetupSkill)
|
||||
}
|
||||
}
|
||||
|
||||
func updateComponent(comp *manifest.Component, home *config.HomeConfig) {
|
||||
fmt.Printf("Updating %s...\n", comp.Name)
|
||||
|
||||
if dryRun {
|
||||
fmt.Printf(" [dry-run] Would update %s\n", comp.Name)
|
||||
return
|
||||
}
|
||||
|
||||
if comp.Source != nil {
|
||||
updateRepo(comp, home)
|
||||
}
|
||||
}
|
||||
|
||||
func cloneOrUpdate(comp *manifest.Component, home *config.HomeConfig) {
|
||||
// Implementation using go-git
|
||||
// For now: print instruction
|
||||
fmt.Printf(" [TODO] Clone %s to %s\n", comp.Source.URL, comp.Target)
|
||||
}
|
||||
|
||||
func updateRepo(comp *manifest.Component, home *config.HomeConfig) {
|
||||
fmt.Printf(" [TODO] Update %s\n", comp.Target)
|
||||
}
|
||||
|
||||
func runPostInstall(comp *manifest.Component, home *config.HomeConfig) {
|
||||
// Implementation
|
||||
fmt.Printf(" [TODO] Run post-install script\n")
|
||||
}
|
||||
|
||||
func filterByClient(m *manifest.Manifest, names []string, client string) []string {
|
||||
result := []string{}
|
||||
for _, name := range names {
|
||||
if comp, ok := m.GetComponent(name); ok {
|
||||
if comp.IsApplicable(client) {
|
||||
result = append(result, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func contains(slice []string, s string) bool {
|
||||
for _, item := range slice {
|
||||
if item == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user