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/modules/hello" "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 init() { if err := Registry.Register(hello.Module{}); err != nil { panic(err) } } func main() { root := &cobra.Command{ Use: "archduino", Short: "Archduino installer and toolbox", SilenceUsage: true, SilenceErrors: true, Version: version, } root.AddCommand(newInstallCmd()) if err := root.Execute(); err != nil { fmt.Fprintln(os.Stderr, "error:", err) os.Exit(1) } } func newInstallCmd() *cobra.Command { 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 { 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) // When the user did NOT opt out of TUI (the default), note that // the TUI isn't implemented yet and we're falling back to plain // output. Plan 2 wires up Bubble Tea here; at that point this // branch becomes the TUI setup and the PlainReporter above moves // inside the `noTUI` branch. if !noTUI { 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) }