feat(tui): wizard uses PlanForce preview when Force is set
This commit is contained in:
@@ -77,7 +77,7 @@ func Run(ctx context.Context, reg *module.Registry, initial tui.WizardState) (tu
|
||||
Title("Review plan").
|
||||
DescriptionFunc(func() string {
|
||||
s.Modules = syncSelected(s.Modules, selectedModules)
|
||||
return renderPlanCached(ctx, reg, s.SelectedIDs(), cache)
|
||||
return renderPlanCached(ctx, reg, s.SelectedIDs(), cache, s.Force)
|
||||
}, &selectedModules)),
|
||||
|
||||
huh.NewGroup(huh.NewConfirm().
|
||||
@@ -118,24 +118,35 @@ func syncSelected(mods []tui.ModuleChoice, selected []string) []tui.ModuleChoice
|
||||
return out
|
||||
}
|
||||
|
||||
// planCache memoises rendered Detect+Plan output per module ID so
|
||||
// DescriptionFunc (which fires on every redraw) does not re-run
|
||||
// potentially expensive Detect/Plan calls. Entries are evicted when a
|
||||
// module leaves the current selection.
|
||||
// planCache memoises rendered Detect+Plan (or PlanForce) output per
|
||||
// (module ID, force) tuple so DescriptionFunc (which fires on every
|
||||
// redraw) does not re-run potentially expensive module calls. Including
|
||||
// force in the key ensures a force-mode preview never reuses a non-force
|
||||
// cached plan. Entries are evicted when a module leaves the current
|
||||
// selection.
|
||||
type planCacheEntry struct{ out string }
|
||||
type planCache struct{ entries map[string]planCacheEntry }
|
||||
|
||||
func newPlanCache() *planCache { return &planCache{entries: map[string]planCacheEntry{}} }
|
||||
|
||||
func (c *planCache) get(id string) (string, bool) {
|
||||
e, ok := c.entries[id]
|
||||
func cacheKey(id string, force bool) string {
|
||||
if force {
|
||||
return "force=1|" + id
|
||||
}
|
||||
return "force=0|" + id
|
||||
}
|
||||
|
||||
func (c *planCache) get(id string, force bool) (string, bool) {
|
||||
e, ok := c.entries[cacheKey(id, force)]
|
||||
return e.out, ok
|
||||
}
|
||||
func (c *planCache) put(id, out string) { c.entries[id] = planCacheEntry{out: out} }
|
||||
func (c *planCache) prune(keepIDs []string) {
|
||||
func (c *planCache) put(id string, force bool, out string) {
|
||||
c.entries[cacheKey(id, force)] = planCacheEntry{out: out}
|
||||
}
|
||||
func (c *planCache) prune(keepIDs []string, force bool) {
|
||||
keep := map[string]bool{}
|
||||
for _, id := range keepIDs {
|
||||
keep[id] = true
|
||||
keep[cacheKey(id, force)] = true
|
||||
}
|
||||
for k := range c.entries {
|
||||
if !keep[k] {
|
||||
@@ -147,38 +158,51 @@ func (c *planCache) prune(keepIDs []string) {
|
||||
// renderPlanCached is the cached equivalent of running Detect+Plan for
|
||||
// every selected module and joining the rendered lines. IDs no longer in
|
||||
// the selection are evicted so a deselect+reselect re-runs Detect.
|
||||
func renderPlanCached(ctx context.Context, reg *module.Registry, selectedIDs []string, cache *planCache) string {
|
||||
cache.prune(selectedIDs)
|
||||
func renderPlanCached(ctx context.Context, reg *module.Registry, selectedIDs []string, cache *planCache, force bool) string {
|
||||
cache.prune(selectedIDs, force)
|
||||
if len(selectedIDs) == 0 {
|
||||
return "No modules selected."
|
||||
}
|
||||
var b strings.Builder
|
||||
if force {
|
||||
b.WriteString("⚠ Force mode: all selected modules will be reapplied\n\n")
|
||||
}
|
||||
for _, id := range selectedIDs {
|
||||
if cached, ok := cache.get(id); ok {
|
||||
if cached, ok := cache.get(id, force); ok {
|
||||
b.WriteString(cached)
|
||||
continue
|
||||
}
|
||||
line := renderOne(ctx, reg, id)
|
||||
cache.put(id, line)
|
||||
line := renderOne(ctx, reg, id, force)
|
||||
cache.put(id, force, line)
|
||||
b.WriteString(line)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderOne runs Detect+Plan for a single module and returns a rendered
|
||||
// line. A failing Detect/Plan is shown inline but does not abort.
|
||||
func renderOne(ctx context.Context, reg *module.Registry, id string) string {
|
||||
// renderOne runs Detect+Plan (or PlanForce when force=true) for a single
|
||||
// module and returns a rendered line. A failing Detect/Plan/PlanForce is
|
||||
// shown inline but does not abort.
|
||||
func renderOne(ctx context.Context, reg *module.Registry, id string, force bool) string {
|
||||
m, ok := reg.Get(id)
|
||||
if !ok {
|
||||
return fmt.Sprintf("• %s — (unknown)\n", id)
|
||||
}
|
||||
state, err := m.Detect(ctx)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("• %s — detect failed: %v\n", id, err)
|
||||
}
|
||||
plan, err := m.Plan(ctx, state)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("• %s — plan failed: %v\n", id, err)
|
||||
var plan module.Plan
|
||||
var err error
|
||||
if force {
|
||||
plan, err = m.PlanForce(ctx)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("• %s — plan failed: %v\n", id, err)
|
||||
}
|
||||
} else {
|
||||
state, derr := m.Detect(ctx)
|
||||
if derr != nil {
|
||||
return fmt.Sprintf("• %s — detect failed: %v\n", id, derr)
|
||||
}
|
||||
plan, err = m.Plan(ctx, state)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("• %s — plan failed: %v\n", id, err)
|
||||
}
|
||||
}
|
||||
if plan.IsEmpty() {
|
||||
return fmt.Sprintf("• %s — already up to date\n", id)
|
||||
|
||||
Reference in New Issue
Block a user