diff --git a/cmd/archduino/main.go b/cmd/archduino/main.go index c378b65..4486467 100644 --- a/cmd/archduino/main.go +++ b/cmd/archduino/main.go @@ -1,14 +1,26 @@ package main import ( + "context" "fmt" "os" + "time" "github.com/spf13/cobra" + + "github.com/MDietermann/ArduinoArchSetup/internal/config" + "github.com/MDietermann/ArduinoArchSetup/internal/engine" + "github.com/MDietermann/ArduinoArchSetup/internal/module" + "github.com/MDietermann/ArduinoArchSetup/internal/privilege" + "github.com/MDietermann/ArduinoArchSetup/internal/state" ) var version = "dev" +// Registry is the global module registry. Tasks in Plan 3 will register +// real modules into it via init(). Task 12 registers the hello toy module. +var Registry = module.NewRegistry() + func main() { root := &cobra.Command{ Use: "archduino", @@ -17,9 +29,7 @@ func main() { SilenceErrors: true, Version: version, } - root.AddCommand(newInstallCmd()) - if err := root.Execute(); err != nil { fmt.Fprintln(os.Stderr, "error:", err) os.Exit(1) @@ -27,19 +37,130 @@ func main() { } func newInstallCmd() *cobra.Command { - var noTUI bool - var dryRun bool + var ( + noTUI bool + dryRun bool + halt bool + noSudo bool + only []string + skip []string + overlayPath string + ) cmd := &cobra.Command{ Use: "install", Short: "Install or reconcile the Archduino toolset", RunE: func(cmd *cobra.Command, args []string) error { - // Foundation scaffold: real wiring lands in Task 11. - fmt.Println("archduino install (stub)") - fmt.Printf(" --no-tui=%v --dry-run=%v\n", noTUI, dryRun) + ctx := cmd.Context() + if ctx == nil { + ctx = context.Background() + } + + cfg, err := config.Load(overlayPath) + if err != nil { + return err + } + + selected := resolveSelection(Registry, cfg, only, skip) + + // Acquire sudo unless dry-run or explicitly skipped. + if !dryRun && !noSudo { + k, err := privilege.Acquire(ctx, privilege.ExecSudo{}, 50*time.Second) + if err != nil { + return err + } + defer k.Stop() + } + + reporter := engine.NewPlainReporter(os.Stdout) + if !noTUI { + // Plan 2 replaces this with the Bubble Tea dashboard. For now + // the plain reporter is the only surface. + fmt.Fprintln(os.Stdout, "(TUI not yet implemented — using plain output)") + } + + started := time.Now().UTC() + sum, err := engine.Run(ctx, Registry, engine.RunOptions{ + Selected: selected, + DryRun: dryRun, + HaltOnError: halt, + Reporter: reporter, + }) + ended := time.Now().UTC() + if err != nil { + return err + } + + ok, fail, skipped := sum.Counts() + fmt.Fprintf(os.Stdout, "\nsummary: %d succeeded, %d failed, %d skipped\n", ok, fail, skipped) + for _, r := range sum.Results { + line := fmt.Sprintf(" %-16s %s", r.ID, r.Status) + if r.Err != nil { + line += " — " + r.Err.Error() + } + fmt.Fprintln(os.Stdout, line) + } + + if !dryRun { + if err := writeState(started, ended, sum); err != nil { + fmt.Fprintln(os.Stderr, "warning: state write failed:", err) + } + } + + if fail > 0 { + return fmt.Errorf("%d module(s) failed", fail) + } return nil }, } cmd.Flags().BoolVar(&noTUI, "no-tui", false, "plain-text output, no interactive UI") cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show planned actions without applying") + cmd.Flags().BoolVar(&halt, "halt-on-error", false, "stop at first failure instead of continuing siblings") + cmd.Flags().BoolVar(&noSudo, "no-sudo", false, "skip upfront sudo -v (for tests / containers with passwordless sudo)") + cmd.Flags().StringSliceVar(&only, "only", nil, "comma-separated module IDs to run (exclusive with --skip)") + cmd.Flags().StringSliceVar(&skip, "skip", nil, "comma-separated module IDs to skip") + cmd.Flags().StringVar(&overlayPath, "config", "", "path to overlay.yaml (default: ~/.config/archduino/overlay.yaml)") return cmd } + +func resolveSelection(reg *module.Registry, cfg config.Config, only, skip []string) []string { + all := reg.All() + disabled := map[string]bool{} + for _, d := range cfg.DisabledModules { + disabled[d] = true + } + for _, d := range skip { + disabled[d] = true + } + onlySet := map[string]bool{} + for _, o := range only { + onlySet[o] = true + } + var out []string + for _, m := range all { + id := m.Meta().ID + if disabled[id] { + continue + } + if len(onlySet) > 0 && !onlySet[id] { + continue + } + out = append(out, id) + } + return out +} + +func writeState(started, ended time.Time, sum engine.Summary) error { + s, err := state.DefaultStore() + if err != nil { + return err + } + rec := state.RunRecord{StartedAt: started, EndedAt: ended} + for _, r := range sum.Results { + mr := state.ModuleRecord{ID: r.ID, Status: string(r.Status), At: ended} + if r.Err != nil { + mr.Error = r.Err.Error() + } + rec.Modules = append(rec.Modules, mr) + } + return s.Save(rec, 10) +}