@@ -228,16 +228,55 @@ func installOptionalTools(r *ui.Renderer, vale, docsBuilder, dryRun, force, assu
228228 }
229229 updateDownloadProgress (progress , "docs-builder" , completed , total )
230230 }
231+ // The upstream installer exits 0 whether it installs, skips an existing
232+ // binary, or is declined at its overwrite prompt, so its exit code alone
233+ // cannot tell us what happened. Compare the version either side of it.
234+ before := updates .BinaryVersion ("docs-builder" , "--version" )
231235 if err := bootstrap .InstallDocsBuilderWithProgress (force , assumeYes , callback ); err != nil {
232236 failures = append (failures , fmt .Errorf ("install docs-builder: %w" , err ))
233237 } else {
234- r .Success ("Installed docs-builder." )
238+ progress .Stop ()
239+ level , message := installerOutcome ("docs-builder" , before , updates .BinaryVersion ("docs-builder" , "--version" ))
240+ switch level {
241+ case outcomeMissing :
242+ r .Warn ("%s" , message )
243+ case outcomeUnchanged :
244+ r .Info ("%s" , message )
245+ default :
246+ r .Success ("%s" , message )
247+ }
235248 }
236249 progress .Stop ()
237250 }
238251 return failures
239252}
240253
254+ type installerOutcomeLevel int
255+
256+ const (
257+ outcomeInstalled installerOutcomeLevel = iota
258+ outcomeUpdated
259+ outcomeUnchanged
260+ outcomeMissing
261+ )
262+
263+ // installerOutcome decides what an upstream installer actually did, from the
264+ // tool's reported version either side of the run. The installers exit 0 whether
265+ // they install, skip an existing binary, or are declined at an overwrite
266+ // prompt, so a zero exit on its own must not be reported as an install.
267+ func installerOutcome (tool , before , after string ) (installerOutcomeLevel , string ) {
268+ switch {
269+ case after == "" :
270+ return outcomeMissing , fmt .Sprintf ("The %s installer finished, but no %s binary is on PATH." , tool , tool )
271+ case before != "" && after == before :
272+ return outcomeUnchanged , fmt .Sprintf ("%s left unchanged at %s; the installer did not replace it." , tool , after )
273+ case before == "" :
274+ return outcomeInstalled , fmt .Sprintf ("Installed %s %s." , tool , after )
275+ default :
276+ return outcomeUpdated , fmt .Sprintf ("Updated %s %s to %s." , tool , before , after )
277+ }
278+ }
279+
241280func reportToolFailures (r * ui.Renderer , failures []error ) {
242281 for _ , failure := range failures {
243282 r .Warn ("%v" , failure )
@@ -452,16 +491,16 @@ func commandUpdate(r *ui.Renderer, args []string) error {
452491 // A failed component must not stop the remaining ones, and the refreshed
453492 // status below is most useful precisely when something went wrong.
454493 var toolFailures []error
455- if selected .vale {
456- toolFailures = append (toolFailures , installOptionalTools (r , true , false , * dryRun , * force , false )... )
457- } else {
494+ if ! selected .vale {
458495 r .Info ("Skipping Vale and Elastic Vale rules." )
459496 }
460- if selected .docsBuilder {
461- toolFailures = append (toolFailures , installOptionalTools (r , false , true , * dryRun , * force , false )... )
462- } else {
497+ if ! selected .docsBuilder {
463498 r .Info ("Skipping docs-builder." )
464499 }
500+ // One call for both tools: installOptionalTools prints its own section
501+ // header, so calling it per tool printed "Installing documentation tools"
502+ // twice and read like two separate phases.
503+ toolFailures = append (toolFailures , installOptionalTools (r , selected .vale , selected .docsBuilder , * dryRun , * force , false )... )
465504 reportToolFailures (r , toolFailures )
466505 if * dryRun {
467506 r .Info ("Would refresh documentation tool status after updates." )
0 commit comments