feat(cmd): wire install subcommand to engine with plain reporter

This commit is contained in:
MDietermann
2026-04-13 20:13:50 +02:00
parent e4f945146b
commit 7a382c9bdd

View File

@@ -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)
}