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:
2026-05-06 23:59:09 +03:00
parent 47b41aa2f7
commit 561a7fd66f
12 changed files with 1380 additions and 43 deletions

322
cmd/factory/main.go Normal file
View 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
}