Skip to content

v0.x: Props.IO, and the setup wizards' test seams

Spec 0198, landing in phases. This note grows with them.

Phase 1: the streams on Props

  • Props.IO (props.IO interface, props.StdIO default) is the invocation's stdin, stdout and stderr, and whether a person is at them. A nil field reads as the process's streams through GetIO(); nothing changes for a tool that never sets it. The root fills it from cobra in its pre-run.
  • --accessible / GTB_ACCESSIBLE=true on every GTB-based tool asks for line prompts (huh's accessible mode) instead of a full-screen form. The gtb generator's wizards (generate project, gtb wizard, generate command, generate add-flag) honour it too, and answers can be piped: the runner hands the form one line per read, because huh reads ahead otherwise and lost every answer after the first. One limit remains upstream: huh's accessible mode prompts for every page, including those a previous answer would hide, so the project wizard over a pipe needs an answer for each page in order; a terminal, or the flags, is the reliable route for it.
  • setup.RunForm(ctx, p, form) is how a framework form runs: on the IO's streams, with the IO's accessible decision, refusing a stdin that is neither a terminal nor accessible (setup.ErrNonInteractive).
  • Tests drive real forms through Props.IO with internal/formtest; see the testing guide. The form-creator options come out in phase 2.

Phase 2: the wizards run real forms

Every setup wizard is one huh form run through setup.RunForm. The options that let a caller substitute a form are gone, and so are the parameters that only existed to pass one through:

Removed Was on
ai.FormOption, ai.WithAIForm ai.RunAIInit, ai.NewCmdInitAI, ai.NewAIInitialiser (each loses its opts ...)
forge.AuthFormOption, forge.WithAuthForm, forge.WithAuthForms the single-token initialisers
forge.DualFormOption, forge.WithDualForm, forge.WithDualForms forge.RunBitbucketInit, forge.RunBitbucketInitCmd (each loses its opts ...)
forge.WithSSHKeySelectForm, forge.WithSSHKeyPathForm, forge.WithPassphraseForm, forge.WithUploadConfirmForm, forge.GenerateKeyOption, forge.WithGenerateKeyOptions, forge.WithSSHForms the SSH stage; forge.ConfigureSSHKey now takes a context.Context first
telemetry.FormOption, telemetry.WithForm telemetry.NewTelemetryInitialiser (loses its opts ...)
root.OutdatedVersionOption, root.WithForm, root.WithInteractive, root.ConsentOption, root.WithConsentInteractive the pre-run's update and consent prompts

A test that used one of these sets Props.IO instead and answers the real form; see the testing guide.

Three wizards now refuse a value huh's accessible mode could not collect (a password prompt with no terminal behind it is left blank, silently): forge.ErrNoTokenEntered, forge.ErrCredentialsIncomplete and forge.ErrPassphraseTooShort.

Phase 3: nothing in pkg/ names the process's streams

  • utils.IsInteractive is removed, and with it the pkg/utils package (it was the last thing in it). Ask the Props: p.GetIO().Interactive(). It read the process's stdin, which is not the invocation's under a test or an embedding, and it said "terminal" of a /dev/null stdin.
  • setup.InitOptions.Interactive is removed: setup.Initialise reads p.GetIO().Interactive(). A test sets Props.IO.
  • config.WithInteractiveCheck on config.NewCmdEdit is removed for the same reason; the editor subprocess runs on the IO's streams.
  • credentialposture.DiscoverModeEnvironment and credentialposture.StorageModeOptions take the IO after the context; setup.StorageModeGroup takes the *props.Props after the context.
  • setup.RunFormOn(ctx, io, form) is RunForm for a caller holding the streams rather than the Props; setup.WithIO(io) hands them to an updater built without Props (setup.NewOfflineUpdater).
  • SelfUpdater.UpdateFromFile takes a context.Context first, and update.Updater with it.
  • The dependency-substitution options are unexported or removed (spec 0198 D4, a clean break). Where a tool used one, the props.Tool field is the way to say the same thing:
Gone Instead
setup.WithReleaseProvider(p) props.Tool.ReleaseProvider = p
setup.WithEmbeddedKeys(k...) props.Tool.Signing.EmbeddedKeys = [][]byte{k...}
setup.WithRequireChecksum(b) props.Tool.Signing.RequireChecksum = &b
setup.WithKeyResolver(r) none: the resolver chain is built from update.key_source; ask for a Tool.Signing field if you need another source
setup.WithMaxBinaryDownloadSize, setup.WithMaxChecksumsSize none: the bounds are the Default* constants; ask for a Tool field if a release exceeds them
setup.WithAuthCheck(p, keys...) write the setup.Middleware yourself; the middleware how-to shows the twelve lines
forge.WithProviderFactory, forge.WithKeyManager, forge.WithSSHOptions, setup.WithExecLookPath, setup.WithOsExecutable in-package test seams; nothing for a tool to do