feat(cmd): wire install subcommand to engine with plain reporter
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user