Repository: cli/cli
Pull request #7816 (2023-08-09 00:56:57)

port repo edit prompts
This PR ports `repo edit` to the new prompter. While doing this I noticed a severe lack of test coverage and some dead code so I dealt with that.

Part of #6255



Patch:
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index eed468389..3b2584063 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -23,7 +23,7 @@ Please avoid:
 ## Building the project
 
 Prerequisites:
-- Go 1.19+
+- Go 1.16+
 
 Build with:
 * Unix-like systems: `make`
diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go
index c55b92400..d66aff065 100644
--- a/internal/codespaces/api/api.go
+++ b/internal/codespaces/api/api.go
@@ -1,12 +1,13 @@
 package api
 
 // For descriptions of service interfaces, see:
+// - https://online.visualstudio.com/api/swagger (for visualstudio.com)
 // - https://docs.github.com/en/rest/reference/repos (for api.github.com)
 // - https://github.com/github/github/blob/master/app/api/codespaces.rb (for vscs_internal)
 // TODO(adonovan): replace the last link with a public doc URL when available.
 
 // TODO(adonovan): a possible reorganization would be to split this
-// file into two internal packages, one per backend service, and to
+// file into three internal packages, one per backend service, and to
 // rename api.API to github.Client:
 //
 // - github.GetUser(github.Client)
@@ -19,6 +20,7 @@ package api
 // - codespaces.GetToken(Client, login, name)
 // - codespaces.List(Client, user)
 // - codespaces.Start(Client, token, codespace)
+// - visualstudio.GetRegionLocation(http.Client) // no dependency on github
 //
 // This would make the meaning of each operation clearer.
 
@@ -32,7 +34,6 @@ import (
 	"io"
 	"net/http"
 	"net/url"
-	"os"
 	"reflect"
 	"regexp"
 	"strconv"
@@ -41,14 +42,13 @@ import (
 
 	"github.com/cenkalti/backoff/v4"
 	"github.com/cli/cli/v2/api"
-	"github.com/cli/cli/v2/internal/ghinstance"
-	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/opentracing/opentracing-go"
 )
 
 const (
-	defaultAPIURL    = "https://api.github.com"
-	defaultServerURL = "https://github.com"
+	githubServer = "https://github.com"
+	githubAPI    = "https://api.github.com"
+	vscsAPI      = "https://online.visualstudio.com"
 )
 
 const (
@@ -60,40 +60,31 @@ const (
 
 // API is the interface to the codespace service.
 type API struct {
-	client       func() (*http.Client, error)
+	client       httpClient
+	vscsAPI      string
 	githubAPI    string
 	githubServer string
 	retryBackoff time.Duration
 }
 
+type httpClient interface {
+	Do(req *http.Request) (*http.Response, error)
+}
+
 // New creates a new API client connecting to the configured endpoints with the HTTP client.
-func New(f *cmdutil.Factory) *API {
-	apiURL := os.Getenv("GITHUB_API_URL")
+func New(serverURL, apiURL, vscsURL string, httpClient httpClient) *API {
+	if serverURL == "" {
+		serverURL = githubServer
+	}
 	if apiURL == "" {
-		cfg, err := f.Config()
-		if err != nil {
-			// fallback to the default api endpoint
-			apiURL = defaultAPIURL
-		} else {
-			host, _ := cfg.Authentication().DefaultHost()
-			apiURL = ghinstance.RESTPrefix(host)
-		}
+		apiURL = githubAPI
 	}
-
-	serverURL := os.Getenv("GITHUB_SERVER_URL")
-	if serverURL == "" {
-		cfg, err := f.Config()
-		if err != nil {
-			// fallback to the default server endpoint
-			serverURL = defaultServerURL
-		} else {
-			host, _ := cfg.Authentication().DefaultHost()
-			serverURL = ghinstance.HostPrefix(host)
-		}
+	if vscsURL == "" {
+		vscsURL = vscsAPI
 	}
-
 	return &API{
-		client:       f.HttpClient,
+		client:       httpClient,
+		vscsAPI:      strings.TrimSuffix(vscsURL, "/"),
 		githubAPI:    strings.TrimSuffix(apiURL, "/"),
 		githubServer: strings.TrimSuffix(serverURL, "/"),
 		retryBackoff: 100 * time.Millisecond,
@@ -106,11 +97,6 @@ type User struct {
 	Type  string `json:"type"`
 }
 
-// ServerURL returns the server url (not the API url), such as https://github.com
-func (a *API) ServerURL() string {
-	return a.githubServer
-}
-
 // GetUser returns the user associated with the given token.
 func (a *API) GetUser(ctx context.Context) (*User, error) {
 	req, err := http.NewRequest(http.MethodGet, a.githubAPI+"/user", nil)
@@ -1133,13 +1119,7 @@ func (a *API) do(ctx context.Context, req *http.Request, spanName string) (*http
 	span, ctx := opentracing.StartSpanFromContext(ctx, spanName)
 	defer span.Finish()
 	req = req.WithContext(ctx)
-
-	httpClient, err := a.client()
-	if err != nil {
-		return nil, err
-	}
-
-	return httpClient.Do(req)
+	return a.client.Do(req)
 }
 
 // setHeaders sets the required headers for the API.
diff --git a/internal/prompter/prompter.go b/internal/prompter/prompter.go
index cd90fcd2d..df6819e1f 100644
--- a/internal/prompter/prompter.go
+++ b/internal/prompter/prompter.go
@@ -14,7 +14,7 @@ import (
 //go:generate moq -rm -out prompter_mock.go . Prompter
 type Prompter interface {
 	Select(string, string, []string) (int, error)
-	MultiSelect(string, []string, []string) ([]int, error)
+	MultiSelect(prompt string, defaults []string, options []string) ([]int, error)
 	Input(string, string) (string, error)
 	InputHostname() (string, error)
 	Password(string) (string, error)
diff --git a/pkg/cmd/codespace/common.go b/pkg/cmd/codespace/common.go
index 006669151..f6f6fb937 100644
--- a/pkg/cmd/codespace/common.go
+++ b/pkg/cmd/codespace/common.go
@@ -88,7 +88,6 @@ func startLiveShareSession(ctx context.Context, codespace *api.Codespace, a *App
 
 //go:generate moq -fmt goimports -rm -skip-ensure -out mock_api.go . apiClient
 type apiClient interface {
-	ServerURL() string
 	GetUser(ctx context.Context) (*api.User, error)
 	GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
 	GetOrgMemberCodespace(ctx context.Context, orgName string, userName string, codespaceName string) (*api.Codespace, error)
diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go
index 889f0b33b..6f973d3cb 100644
--- a/pkg/cmd/codespace/create.go
+++ b/pkg/cmd/codespace/create.go
@@ -129,7 +129,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
 	}
 
 	if opts.useWeb && userInputs.Repository == "" {
-		return a.browser.Browse(fmt.Sprintf("%s/codespaces/new", a.apiClient.ServerURL()))
+		return a.browser.Browse("https://github.com/codespaces/new")
 	}
 
 	promptForRepoAndBranch := userInputs.Repository == "" && !opts.useWeb
@@ -293,7 +293,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
 	}
 
 	if opts.useWeb {
-		return a.browser.Browse(fmt.Sprintf("%s/codespaces/new?repo=%d&ref=%s&machine=%s&location=%s", a.apiClient.ServerURL(), createParams.RepositoryID, createParams.Branch, createParams.Machine, createParams.Location))
+		return a.browser.Browse(fmt.Sprintf("https://github.com/codespaces/new?repo=%d&ref=%s&machine=%s&location=%s", createParams.RepositoryID, createParams.Branch, createParams.Machine, createParams.Location))
 	}
 
 	var codespace *api.Codespace
diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go
index 4775e3db5..bba34ff45 100644
--- a/pkg/cmd/codespace/list.go
+++ b/pkg/cmd/codespace/list.go
@@ -79,7 +79,7 @@ func newListCmd(app *App) *cobra.Command {
 
 func (a *App) List(ctx context.Context, opts *listOptions, exporter cmdutil.Exporter) error {
 	if opts.useWeb && opts.repo == "" {
-		return a.browser.Browse(fmt.Sprintf("%s/codespaces", a.apiClient.ServerURL()))
+		return a.browser.Browse("https://github.com/codespaces")
 	}
 
 	var codespaces []*api.Codespace
@@ -113,7 +113,7 @@ func (a *App) List(ctx context.Context, opts *listOptions, exporter cmdutil.Expo
 	}
 
 	if opts.useWeb && codespaces[0].Repository.ID > 0 {
-		return a.browser.Browse(fmt.Sprintf("%s/codespaces?repository_id=%d", a.apiClient.ServerURL(), codespaces[0].Repository.ID))
+		return a.browser.Browse(fmt.Sprintf("https://github.com/codespaces?repository_id=%d", codespaces[0].Repository.ID))
 	}
 
 	//nolint:staticcheck // SA1019: utils.NewTablePrinter is deprecated: use internal/tableprinter
diff --git a/pkg/cmd/codespace/mock_api.go b/pkg/cmd/codespace/mock_api.go
index 7e211fd7c..0796679fc 100644
--- a/pkg/cmd/codespace/mock_api.go
+++ b/pkg/cmd/codespace/mock_api.go
@@ -7,7 +7,7 @@ import (
 	"context"
 	"sync"
 
-	codespacesAPI "github.com/cli/cli/v2/internal/codespaces/api"
+	"github.com/cli/cli/v2/internal/codespaces/api"
 )
 
 // apiClientMock is a mock implementation of apiClient.
@@ -16,46 +16,43 @@ import (
 //
 //		// make and configure a mocked apiClient
 //		mockedapiClient := &apiClientMock{
-//			CreateCodespaceFunc: func(ctx context.Context, params *codespacesAPI.CreateCodespaceParams) (*codespacesAPI.Codespace, error) {
+//			CreateCodespaceFunc: func(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) {
 //				panic("mock out the CreateCodespace method")
 //			},
 //			DeleteCodespaceFunc: func(ctx context.Context, name string, orgName string, userName string) error {
 //				panic("mock out the DeleteCodespace method")
 //			},
-//			EditCodespaceFunc: func(ctx context.Context, codespaceName string, params *codespacesAPI.EditCodespaceParams) (*codespacesAPI.Codespace, error) {
+//			EditCodespaceFunc: func(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error) {
 //				panic("mock out the EditCodespace method")
 //			},
-//			GetCodespaceFunc: func(ctx context.Context, name string, includeConnection bool) (*codespacesAPI.Codespace, error) {
+//			GetCodespaceFunc: func(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) {
 //				panic("mock out the GetCodespace method")
 //			},
-//			GetCodespaceBillableOwnerFunc: func(ctx context.Context, nwo string) (*codespacesAPI.User, error) {
+//			GetCodespaceBillableOwnerFunc: func(ctx context.Context, nwo string) (*api.User, error) {
 //				panic("mock out the GetCodespaceBillableOwner method")
 //			},
-//			GetCodespaceRepoSuggestionsFunc: func(ctx context.Context, partialSearch string, params codespacesAPI.RepoSearchParameters) ([]string, error) {
+//			GetCodespaceRepoSuggestionsFunc: func(ctx context.Context, partialSearch string, params api.RepoSearchParameters) ([]string, error) {
 //				panic("mock out the GetCodespaceRepoSuggestions method")
 //			},
-//			GetCodespaceRepositoryContentsFunc: func(ctx context.Context, codespace *codespacesAPI.Codespace, path string) ([]byte, error) {
+//			GetCodespaceRepositoryContentsFunc: func(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error) {
 //				panic("mock out the GetCodespaceRepositoryContents method")
 //			},
-//			GetCodespacesMachinesFunc: func(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*codespacesAPI.Machine, error) {
+//			GetCodespacesMachinesFunc: func(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*api.Machine, error) {
 //				panic("mock out the GetCodespacesMachines method")
 //			},
-//			GetOrgMemberCodespaceFunc: func(ctx context.Context, orgName string, userName string, codespaceName string) (*codespacesAPI.Codespace, error) {
+//			GetOrgMemberCodespaceFunc: func(ctx context.Context, orgName string, userName string, codespaceName string) (*api.Codespace, error) {
 //				panic("mock out the GetOrgMemberCodespace method")
 //			},
-//			GetRepositoryFunc: func(ctx context.Context, nwo string) (*codespacesAPI.Repository, error) {
+//			GetRepositoryFunc: func(ctx context.Context, nwo string) (*api.Repository, error) {
 //				panic("mock out the GetRepository method")
 //			},
-//			ServerURLFunc: func() string {
-//				panic("mock out the ServerURL method")
-//			},
-//			GetUserFunc: func(ctx context.Context) (*codespacesAPI.User, error) {
+//			GetUserFunc: func(ctx context.Context) (*api.User, error) {
 //				panic("mock out the GetUser method")
 //			},
-//			ListCodespacesFunc: func(ctx context.Context, opts codespacesAPI.ListCodespacesOptions) ([]*codespacesAPI.Codespace, error) {
+//			ListCodespacesFunc: func(ctx context.Context, opts api.ListCodespacesOptions) ([]*api.Codespace, error) {
 //				panic("mock out the ListCodespaces method")
 //			},
-//			ListDevContainersFunc: func(ctx context.Context, repoID int, branch string, limit int) ([]codespacesAPI.DevContainerEntry, error) {
+//			ListDevContainersFunc: func(ctx context.Context, repoID int, branch string, limit int) ([]api.DevContainerEntry, error) {
 //				panic("mock out the ListDevContainers method")
 //			},
 //			StartCodespaceFunc: func(ctx context.Context, name string) error {
@@ -72,46 +69,43 @@ import (
 //	}
 type apiClientMock struct {
 	// CreateCodespaceFunc mocks the CreateCodespace method.
-	CreateCodespaceFunc func(ctx context.Context, params *codespacesAPI.CreateCodespaceParams) (*codespacesAPI.Codespace, error)
+	CreateCodespaceFunc func(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error)
 
 	// DeleteCodespaceFunc mocks the DeleteCodespace method.
 	DeleteCodespaceFunc func(ctx context.Context, name string, orgName string, userName string) error
 
 	// EditCodespaceFunc mocks the EditCodespace method.
-	EditCodespaceFunc func(ctx context.Context, codespaceName string, params *codespacesAPI.EditCodespaceParams) (*codespacesAPI.Codespace, error)
+	EditCodespaceFunc func(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error)
 
 	// GetCodespaceFunc mocks the GetCodespace method.
-	GetCodespaceFunc func(ctx context.Context, name string, includeConnection bool) (*codespacesAPI.Codespace, error)
+	GetCodespaceFunc func(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
 
 	// GetCodespaceBillableOwnerFunc mocks the GetCodespaceBillableOwner method.
-	GetCodespaceBillableOwnerFunc func(ctx context.Context, nwo string) (*codespacesAPI.User, error)
+	GetCodespaceBillableOwnerFunc func(ctx context.Context, nwo string) (*api.User, error)
 
 	// GetCodespaceRepoSuggestionsFunc mocks the GetCodespaceRepoSuggestions method.
-	GetCodespaceRepoSuggestionsFunc func(ctx context.Context, partialSearch string, params codespacesAPI.RepoSearchParameters) ([]string, error)
+	GetCodespaceRepoSuggestionsFunc func(ctx context.Context, partialSearch string, params api.RepoSearchParameters) ([]string, error)
 
 	// GetCodespaceRepositoryContentsFunc mocks the GetCodespaceRepositoryContents method.
-	GetCodespaceRepositoryContentsFunc func(ctx context.Context, codespace *codespacesAPI.Codespace, path string) ([]byte, error)
+	GetCodespaceRepositoryContentsFunc func(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error)
 
 	// GetCodespacesMachinesFunc mocks the GetCodespacesMachines method.
-	GetCodespacesMachinesFunc func(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*codespacesAPI.Machine, error)
+	GetCodespacesMachinesFunc func(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*api.Machine, error)
 
 	// GetOrgMemberCodespaceFunc mocks the GetOrgMemberCodespace method.
-	GetOrgMemberCodespaceFunc func(ctx context.Context, orgName string, userName string, codespaceName string) (*codespacesAPI.Codespace, error)
+	GetOrgMemberCodespaceFunc func(ctx context.Context, orgName string, userName string, codespaceName string) (*api.Codespace, error)
 
 	// GetRepositoryFunc mocks the GetRepository method.
-	GetRepositoryFunc func(ctx context.Context, nwo string) (*codespacesAPI.Repository, error)
-
-	// ServerURLFunc mocks the ServerURL method.
-	ServerURLFunc func() string
+	GetRepositoryFunc func(ctx context.Context, nwo string) (*api.Repository, error)
 
 	// GetUserFunc mocks the GetUser method.
-	GetUserFunc func(ctx context.Context) (*codespacesAPI.User, error)
+	GetUserFunc func(ctx context.Context) (*api.User, error)
 
 	// ListCodespacesFunc mocks the ListCodespaces method.
-	ListCodespacesFunc func(ctx context.Context, opts codespacesAPI.ListCodespacesOptions) ([]*codespacesAPI.Codespace, error)
+	ListCodespacesFunc func(ctx context.Context, opts api.ListCodespacesOptions) ([]*api.Codespace, error)
 
 	// ListDevContainersFunc mocks the ListDevContainers method.
-	ListDevContainersFunc func(ctx context.Context, repoID int, branch string, limit int) ([]codespacesAPI.DevContainerEntry, error)
+	ListDevContainersFunc func(ctx context.Context, repoID int, branch string, limit int) ([]api.DevContainerEntry, error)
 
 	// StartCodespaceFunc mocks the StartCodespace method.
 	StartCodespaceFunc func(ctx context.Context, name string) error
@@ -126,7 +120,7 @@ type apiClientMock struct {
 			// Ctx is the ctx argument value.
 			Ctx context.Context
 			// Params is the params argument value.
-			Params *codespacesAPI.CreateCodespaceParams
+			Params *api.CreateCodespaceParams
 		}
 		// DeleteCodespace holds details about calls to the DeleteCodespace method.
 		DeleteCodespace []struct {
@@ -146,7 +140,7 @@ type apiClientMock struct {
 			// CodespaceName is the codespaceName argument value.
 			CodespaceName string
 			// Params is the params argument value.
-			Params *codespacesAPI.EditCodespaceParams
+			Params *api.EditCodespaceParams
 		}
 		// GetCodespace holds details about calls to the GetCodespace method.
 		GetCodespace []struct {
@@ -171,14 +165,14 @@ type apiClientMock struct {
 			// PartialSearch is the partialSearch argument value.
 			PartialSearch string
 			// Params is the params argument value.
-			Params codespacesAPI.RepoSearchParameters
+			Params api.RepoSearchParameters
 		}
 		// GetCodespaceRepositoryContents holds details about calls to the GetCodespaceRepositoryContents method.
 		GetCodespaceRepositoryContents []struct {
 			// Ctx is the ctx argument value.
 			Ctx context.Context
 			// Codespace is the codespace argument value.
-			Codespace *codespacesAPI.Codespace
+			Codespace *api.Codespace
 			// Path is the path argument value.
 			Path string
 		}
@@ -213,9 +207,6 @@ type apiClientMock struct {
 			// Nwo is the nwo argument value.
 			Nwo string
 		}
-		// ServerURL holds details about calls to the ServerURL method.
-		ServerURL []struct {
-		}
 		// GetUser holds details about calls to the GetUser method.
 		GetUser []struct {
 			// Ctx is the ctx argument value.
@@ -226,7 +217,7 @@ type apiClientMock struct {
 			// Ctx is the ctx argument value.
 			Ctx context.Context
 			// Opts is the opts argument value.
-			Opts codespacesAPI.ListCodespacesOptions
+			Opts api.ListCodespacesOptions
 		}
 		// ListDevContainers holds details about calls to the ListDevContainers method.
 		ListDevContainers []struct {
@@ -268,7 +259,6 @@ type apiClientMock struct {
 	lockGetCodespacesMachines          sync.RWMutex
 	lockGetOrgMemberCodespace          sync.RWMutex
 	lockGetRepository                  sync.RWMutex
-	lockServerURL                      sync.RWMutex
 	lockGetUser                        sync.RWMutex
 	lockListCodespaces                 sync.RWMutex
 	lockListDevContainers              sync.RWMutex
@@ -277,13 +267,13 @@ type apiClientMock struct {
 }
 
 // CreateCodespace calls CreateCodespaceFunc.
-func (mock *apiClientMock) CreateCodespace(ctx context.Context, params *codespacesAPI.CreateCodespaceParams) (*codespacesAPI.Codespace, error) {
+func (mock *apiClientMock) CreateCodespace(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) {
 	if mock.CreateCodespaceFunc == nil {
 		panic("apiClientMock.CreateCodespaceFunc: method is nil but apiClient.CreateCodespace was just called")
 	}
 	callInfo := struct {
 		Ctx    context.Context
-		Params *codespacesAPI.CreateCodespaceParams
+		Params *api.CreateCodespaceParams
 	}{
 		Ctx:    ctx,
 		Params: params,
@@ -300,11 +290,11 @@ func (mock *apiClientMock) CreateCodespace(ctx context.Context, params *codespac
 //	len(mockedapiClient.CreateCodespaceCalls())
 func (mock *apiClientMock) CreateCodespaceCalls() []struct {
 	Ctx    context.Context
-	Params *codespacesAPI.CreateCodespaceParams
+	Params *api.CreateCodespaceParams
 } {
 	var calls []struct {
 		Ctx    context.Context
-		Params *codespacesAPI.CreateCodespaceParams
+		Params *api.CreateCodespaceParams
 	}
 	mock.lockCreateCodespace.RLock()
 	calls = mock.calls.CreateCodespace
@@ -357,14 +347,14 @@ func (mock *apiClientMock) DeleteCodespaceCalls() []struct {
 }
 
 // EditCodespace calls EditCodespaceFunc.
-func (mock *apiClientMock) EditCodespace(ctx context.Context, codespaceName string, params *codespacesAPI.EditCodespaceParams) (*codespacesAPI.Codespace, error) {
+func (mock *apiClientMock) EditCodespace(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error) {
 	if mock.EditCodespaceFunc == nil {
 		panic("apiClientMock.EditCodespaceFunc: method is nil but apiClient.EditCodespace was just called")
 	}
 	callInfo := struct {
 		Ctx           context.Context
 		CodespaceName string
-		Params        *codespacesAPI.EditCodespaceParams
+		Params        *api.EditCodespaceParams
 	}{
 		Ctx:           ctx,
 		CodespaceName: codespaceName,
@@ -383,12 +373,12 @@ func (mock *apiClientMock) EditCodespace(ctx context.Context, codespaceName stri
 func (mock *apiClientMock) EditCodespaceCalls() []struct {
 	Ctx           context.Context
 	CodespaceName string
-	Params        *codespacesAPI.EditCodespaceParams
+	Params        *api.EditCodespaceParams
 } {
 	var calls []struct {
 		Ctx           context.Context
 		CodespaceName string
-		Params        *codespacesAPI.EditCodespaceParams
+		Params        *api.EditCodespaceParams
 	}
 	mock.lockEditCodespace.RLock()
 	calls = mock.calls.EditCodespace
@@ -397,7 +387,7 @@ func (mock *apiClientMock) EditCodespaceCalls() []struct {
 }
 
 // GetCodespace calls GetCodespaceFunc.
-func (mock *apiClientMock) GetCodespace(ctx context.Context, name string, includeConnection bool) (*codespacesAPI.Codespace, error) {
+func (mock *apiClientMock) GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) {
 	if mock.GetCodespaceFunc == nil {
 		panic("apiClientMock.GetCodespaceFunc: method is nil but apiClient.GetCodespace was just called")
 	}
@@ -437,7 +427,7 @@ func (mock *apiClientMock) GetCodespaceCalls() []struct {
 }
 
 // GetCodespaceBillableOwner calls GetCodespaceBillableOwnerFunc.
-func (mock *apiClientMock) GetCodespaceBillableOwner(ctx context.Context, nwo string) (*codespacesAPI.User, error) {
+func (mock *apiClientMock) GetCodespaceBillableOwner(ctx context.Context, nwo string) (*api.User, error) {
 	if mock.GetCodespaceBillableOwnerFunc == nil {
 		panic("apiClientMock.GetCodespaceBillableOwnerFunc: method is nil but apiClient.GetCodespaceBillableOwner was just called")
 	}
@@ -473,14 +463,14 @@ func (mock *apiClientMock) GetCodespaceBillableOwnerCalls() []struct {
 }
 
 // GetCodespaceRepoSuggestions calls GetCodespaceRepoSuggestionsFunc.
-func (mock *apiClientMock) GetCodespaceRepoSuggestions(ctx context.Context, partialSearch string, params codespacesAPI.RepoSearchParameters) ([]string, error) {
+func (mock *apiClientMock) GetCodespaceRepoSuggestions(ctx context.Context, partialSearch string, params api.RepoSearchParameters) ([]string, error) {
 	if mock.GetCodespaceRepoSuggestionsFunc == nil {
 		panic("apiClientMock.GetCodespaceRepoSuggestionsFunc: method is nil but apiClient.GetCodespaceRepoSuggestions was just called")
 	}
 	callInfo := struct {
 		Ctx           context.Context
 		PartialSearch string
-		Params        codespacesAPI.RepoSearchParameters
+		Params        api.RepoSearchParameters
 	}{
 		Ctx:           ctx,
 		PartialSearch: partialSearch,
@@ -499,12 +489,12 @@ func (mock *apiClientMock) GetCodespaceRepoSuggestions(ctx context.Context, part
 func (mock *apiClientMock) GetCodespaceRepoSuggestionsCalls() []struct {
 	Ctx           context.Context
 	PartialSearch string
-	Params        codespacesAPI.RepoSearchParameters
+	Params        api.RepoSearchParameters
 } {
 	var calls []struct {
 		Ctx           context.Context
 		PartialSearch string
-		Params        codespacesAPI.RepoSearchParameters
+		Params        api.RepoSearchParameters
 	}
 	mock.lockGetCodespaceRepoSuggestions.RLock()
 	calls = mock.calls.GetCodespaceRepoSuggestions
@@ -513,13 +503,13 @@ func (mock *apiClientMock) GetCodespaceRepoSuggestionsCalls() []struct {
 }
 
 // GetCodespaceRepositoryContents calls GetCodespaceRepositoryContentsFunc.
-func (mock *apiClientMock) GetCodespaceRepositoryContents(ctx context.Context, codespace *codespacesAPI.Codespace, path string) ([]byte, error) {
+func (mock *apiClientMock) GetCodespaceRepositoryContents(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error) {
 	if mock.GetCodespaceRepositoryContentsFunc == nil {
 		panic("apiClientMock.GetCodespaceRepositoryContentsFunc: method is nil but apiClient.GetCodespaceRepositoryContents was just called")
 	}
 	callInfo := struct {
 		Ctx       context.Context
-		Codespace *codespacesAPI.Codespace
+		Codespace *api.Codespace
 		Path      string
 	}{
 		Ctx:       ctx,
@@ -538,12 +528,12 @@ func (mock *apiClientMock) GetCodespaceRepositoryContents(ctx context.Context, c
 //	len(mockedapiClient.GetCodespaceRepositoryContentsCalls())
 func (mock *apiClientMock) GetCodespaceRepositoryContentsCalls() []struct {
 	Ctx       context.Context
-	Codespace *codespacesAPI.Codespace
+	Codespace *api.Codespace
 	Path      string
 } {
 	var calls []struct {
 		Ctx       context.Context
-		Codespace *codespacesAPI.Codespace
+		Codespace *api.Codespace
 		Path      string
 	}
 	mock.lockGetCodespaceRepositoryContents.RLock()
@@ -553,7 +543,7 @@ func (mock *apiClientMock) GetCodespaceRepositoryContentsCalls() []struct {
 }
 
 // GetCodespacesMachines calls GetCodespacesMachinesFunc.
-func (mock *apiClientMock) GetCodespacesMachines(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*codespacesAPI.Machine, error) {
+func (mock *apiClientMock) GetCodespacesMachines(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*api.Machine, error) {
 	if mock.GetCodespacesMachinesFunc == nil {
 		panic("apiClientMock.GetCodespacesMachinesFunc: method is nil but apiClient.GetCodespacesMachines was just called")
 	}
@@ -601,7 +591,7 @@ func (mock *apiClientMock) GetCodespacesMachinesCalls() []struct {
 }
 
 // GetOrgMemberCodespace calls GetOrgMemberCodespaceFunc.
-func (mock *apiClientMock) GetOrgMemberCodespace(ctx context.Context, orgName string, userName string, codespaceName string) (*codespacesAPI.Codespace, error) {
+func (mock *apiClientMock) GetOrgMemberCodespace(ctx context.Context, orgName string, userName string, codespaceName string) (*api.Codespace, error) {
 	if mock.GetOrgMemberCodespaceFunc == nil {
 		panic("apiClientMock.GetOrgMemberCodespaceFunc: method is nil but apiClient.GetOrgMemberCodespace was just called")
 	}
@@ -645,7 +635,7 @@ func (mock *apiClientMock) GetOrgMemberCodespaceCalls() []struct {
 }
 
 // GetRepository calls GetRepositoryFunc.
-func (mock *apiClientMock) GetRepository(ctx context.Context, nwo string) (*codespacesAPI.Repository, error) {
+func (mock *apiClientMock) GetRepository(ctx context.Context, nwo string) (*api.Repository, error) {
 	if mock.GetRepositoryFunc == nil {
 		panic("apiClientMock.GetRepositoryFunc: method is nil but apiClient.GetRepository was just called")
 	}
@@ -680,35 +670,8 @@ func (mock *apiClientMock) GetRepositoryCalls() []struct {
 	return calls
 }
 
-// ServerURL calls ServerURLFunc.
-func (mock *apiClientMock) ServerURL() string {
-	if mock.ServerURLFunc == nil {
-		panic("apiClientMock.ServerURLFunc: method is nil but apiClient.ServerURL was just called")
-	}
-	callInfo := struct {
-	}{}
-	mock.lockServerURL.Lock()
-	mock.calls.ServerURL = append(mock.calls.ServerURL, callInfo)
-	mock.lockServerURL.Unlock()
-	return mock.ServerURLFunc()
-}
-
-// ServerURLCalls gets all the calls that were made to ServerURL.
-// Check the length with:
-//
-//	len(mockedapiClient.ServerURLCalls())
-func (mock *apiClientMock) ServerURLCalls() []struct {
-} {
-	var calls []struct {
-	}
-	mock.lockServerURL.RLock()
-	calls = mock.calls.ServerURL
-	mock.lockServerURL.RUnlock()
-	return calls
-}
-
 // GetUser calls GetUserFunc.
-func (mock *apiClientMock) GetUser(ctx context.Context) (*codespacesAPI.User, error) {
+func (mock *apiClientMock) GetUser(ctx context.Context) (*api.User, error) {
 	if mock.GetUserFunc == nil {
 		panic("apiClientMock.GetUserFunc: method is nil but apiClient.GetUser was just called")
 	}
@@ -740,13 +703,13 @@ func (mock *apiClientMock) GetUserCalls() []struct {
 }
 
 // ListCodespaces calls ListCodespacesFunc.
-func (mock *apiClientMock) ListCodespaces(ctx context.Context, opts codespacesAPI.ListCodespacesOptions) ([]*codespacesAPI.Codespace, error) {
+func (mock *apiClientMock) ListCodespaces(ctx context.Context, opts api.ListCodespacesOptions) ([]*api.Codespace, error) {
 	if mock.ListCodespacesFunc == nil {
 		panic("apiClientMock.ListCodespacesFunc: method is nil but apiClient.ListCodespaces was just called")
 	}
 	callInfo := struct {
 		Ctx  context.Context
-		Opts codespacesAPI.ListCodespacesOptions
+		Opts api.ListCodespacesOptions
 	}{
 		Ctx:  ctx,
 		Opts: opts,
@@ -763,11 +726,11 @@ func (mock *apiClientMock) ListCodespaces(ctx context.Context, opts codespacesAP
 //	len(mockedapiClient.ListCodespacesCalls())
 func (mock *apiClientMock) ListCodespacesCalls() []struct {
 	Ctx  context.Context
-	Opts codespacesAPI.ListCodespacesOptions
+	Opts api.ListCodespacesOptions
 } {
 	var calls []struct {
 		Ctx  context.Context
-		Opts codespacesAPI.ListCodespacesOptions
+		Opts api.ListCodespacesOptions
 	}
 	mock.lockListCodespaces.RLock()
 	calls = mock.calls.ListCodespaces
@@ -776,7 +739,7 @@ func (mock *apiClientMock) ListCodespacesCalls() []struct {
 }
 
 // ListDevContainers calls ListDevContainersFunc.
-func (mock *apiClientMock) ListDevContainers(ctx context.Context, repoID int, branch string, limit int) ([]codespacesAPI.DevContainerEntry, error) {
+func (mock *apiClientMock) ListDevContainers(ctx context.Context, repoID int, branch string, limit int) ([]api.DevContainerEntry, error) {
 	if mock.ListDevContainersFunc == nil {
 		panic("apiClientMock.ListDevContainersFunc: method is nil but apiClient.ListDevContainers was just called")
 	}
diff --git a/pkg/cmd/codespace/root.go b/pkg/cmd/codespace/root.go
index d1675a8f7..28a80edae 100644
--- a/pkg/cmd/codespace/root.go
+++ b/pkg/cmd/codespace/root.go
@@ -1,28 +1,15 @@
 package codespace
 
 import (
-	codespacesAPI "github.com/cli/cli/v2/internal/codespaces/api"
-
-	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/spf13/cobra"
 )
 
-func NewCmdCodespace(f *cmdutil.Factory) *cobra.Command {
+func NewRootCmd(app *App) *cobra.Command {
 	root := &cobra.Command{
-		Use:     "codespace",
-		Short:   "Connect to and manage codespaces",
-		Aliases: []string{"cs"},
-		GroupID: "core",
+		Use:   "codespace",
+		Short: "Connect to and manage codespaces",
 	}
 
-	app := NewApp(
-		f.IOStreams,
-		f,
-		codespacesAPI.New(f),
-		f.Browser,
-		f.Remotes,
-	)
-
 	root.AddCommand(newCodeCmd(app))
 	root.AddCommand(newCreateCmd(app))
 	root.AddCommand(newEditCmd(app))
diff --git a/pkg/cmd/repo/edit/edit.go b/pkg/cmd/repo/edit/edit.go
index bbbd94dd5..112aa5131 100644
--- a/pkg/cmd/repo/edit/edit.go
+++ b/pkg/cmd/repo/edit/edit.go
@@ -10,21 +10,25 @@ import (
 	"strings"
 	"time"
 
-	"github.com/AlecAivazis/survey/v2"
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/api"
 	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/ghinstance"
 	"github.com/cli/cli/v2/internal/ghrepo"
-	"github.com/cli/cli/v2/internal/prompter"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
-	"github.com/cli/cli/v2/pkg/prompt"
 	"github.com/cli/cli/v2/pkg/set"
 	"github.com/spf13/cobra"
 	"golang.org/x/sync/errgroup"
 )
 
+type iprompter interface {
+	MultiSelect(prompt string, defaults []string, options []string) ([]int, error)
+	Input(string, string) (string, error)
+	Confirm(string, bool) (bool, error)
+	Select(string, string, []string) (int, error)
+}
+
 const (
 	allowMergeCommits = "Allow Merge Commits"
 	allowSquashMerge  = "Allow Squash Merging"
@@ -53,7 +57,7 @@ type EditOptions struct {
 	RemoveTopics    []string
 	InteractiveMode bool
 	Detector        fd.Detector
-	Prompter        prompter.Prompter
+	Prompter        iprompter
 	// Cache of current repo topics to avoid retrieving them
 	// in multiple flows.
 	topicsCache []string
@@ -279,7 +283,7 @@ func editRun(ctx context.Context, opts *EditOptions) error {
 	return nil
 }
 
-func interactiveChoice(r *api.Repository) ([]string, error) {
+func interactiveChoice(p iprompter, r *api.Repository) ([]string, error) {
 	options := []string{
 		optionDefaultBranchName,
 		optionDescription,
@@ -298,11 +302,14 @@ func interactiveChoice(r *api.Repository) ([]string, error) {
 		options = append(options, optionAllowForking)
 	}
 	var answers []string
-	//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-	err := prompt.SurveyAskOne(&survey.MultiSelect{
-		Message: "What do you want to edit?",
-		Options: options,
-	}, &answers, survey.WithPageSize(11))
+	selected, err := p.MultiSelect("What do you want to edit?", nil, options)
+	if err != nil {
+		return nil, err
+	}
+	for _, i := range selected {
+		answers = append(answers, options[i])
+	}
+
 	return answers, err
 }
 
@@ -310,38 +317,27 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
 	for _, v := range r.RepositoryTopics.Nodes {
 		opts.topicsCache = append(opts.topicsCache, v.Topic.Name)
 	}
-	choices, err := interactiveChoice(r)
+	p := opts.Prompter
+	choices, err := interactiveChoice(p, r)
 	if err != nil {
 		return err
 	}
 	for _, c := range choices {
 		switch c {
 		case optionDescription:
-			opts.Edits.Description = &r.Description
-			//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-			err = prompt.SurveyAskOne(&survey.Input{
-				Message: "Description of the repository",
-				Default: r.Description,
-			}, opts.Edits.Description)
+			answer, err := p.Input("Description of the repository", r.Description)
 			if err != nil {
 				return err
 			}
+			opts.Edits.Description = &answer
 		case optionHomePageURL:
-			opts.Edits.Homepage = &r.HomepageURL
-			//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-			err = prompt.SurveyAskOne(&survey.Input{
-				Message: "Repository home page URL",
-				Default: r.HomepageURL,
-			}, opts.Edits.Homepage)
+			a, err := p.Input("Repository home page URL", r.HomepageURL)
 			if err != nil {
 				return err
 			}
+			opts.Edits.Homepage = &a
 		case optionTopics:
-			var addTopics string
-			//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-			err = prompt.SurveyAskOne(&survey.Input{
-				Message: "Add topics?(csv format)",
-			}, &addTopics)
+			addTopics, err := p.Input("Add topics?(csv format)", "")
 			if err != nil {
 				return err
 			}
@@ -350,69 +346,41 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
 			}
 
 			if len(opts.topicsCache) > 0 {
-				//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-				err = prompt.SurveyAskOne(&survey.MultiSelect{
-					Message: "Remove Topics",
-					Options: opts.topicsCache,
-				}, &opts.RemoveTopics)
+				selected, err := p.MultiSelect("Remove Topics", nil, opts.topicsCache)
 				if err != nil {
 					return err
 				}
+				for _, i := range selected {
+					opts.RemoveTopics = append(opts.RemoveTopics, opts.topicsCache[i])
+				}
 			}
 		case optionDefaultBranchName:
-			opts.Edits.DefaultBranch = &r.DefaultBranchRef.Name
-			//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-			err = prompt.SurveyAskOne(&survey.Input{
-				Message: "Default branch name",
-				Default: r.DefaultBranchRef.Name,
-			}, opts.Edits.DefaultBranch)
+			name, err := p.Input("Default branch name", r.DefaultBranchRef.Name)
 			if err != nil {
 				return err
 			}
+			opts.Edits.DefaultBranch = &name
 		case optionWikis:
-			opts.Edits.EnableWiki = &r.HasWikiEnabled
-			//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-			err = prompt.SurveyAskOne(&survey.Confirm{
-				Message: "Enable Wikis?",
-				Default: r.HasWikiEnabled,
-			}, opts.Edits.EnableWiki)
+			c, err := p.Confirm("Enable Wikis?", r.HasWikiEnabled)
 			if err != nil {
 				return err
 			}
+			opts.Edits.EnableWiki = &c
 		case optionIssues:
-			opts.Edits.EnableIssues = &r.HasIssuesEnabled
-			//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-			err = prompt.SurveyAskOne(&survey.Confirm{
-				Message: "Enable Issues?",
-				Default: r.HasIssuesEnabled,
-			}, opts.Edits.EnableIssues)
+			a, err := p.Confirm("Enable Issues?", r.HasIssuesEnabled)
 			if err != nil {
 				return err
 			}
+			opts.Edits.EnableIssues = &a
 		case optionProjects:
-			opts.Edits.EnableProjects = &r.HasProjectsEnabled
-			//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-			err = prompt.SurveyAskOne(&survey.Confirm{
-				Message: "Enable Projects?",
-				Default: r.HasProjectsEnabled,
-			}, opts.Edits.EnableProjects)
-			if err != nil {
-				return err
-			}
-		case optionDiscussions:
-			opts.Edits.EnableDiscussions = &r.HasDiscussionsEnabled
-			//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-			err = prompt.SurveyAskOne(&survey.Confirm{
-				Message: "Enable Discussions?",
-				Default: r.HasDiscussionsEnabled,
-			}, opts.Edits.EnableDiscussions)
+			a, err := p.Confirm("Enable Projects?", r.HasProjectsEnabled)
 			if err != nil {
 				return err
 			}
+			opts.Edits.EnableProjects = &a
 		case optionVisibility:
-			opts.Edits.Visibility = &r.Visibility
 			visibilityOptions := []string{"public", "private", "internal"}
-			selected, err := opts.Prompter.Select("Visibility", strings.ToLower(r.Visibility), visibilityOptions)
+			selected, err := p.Select("Visibility", strings.ToLower(r.Visibility), visibilityOptions)
 			if err != nil {
 				return err
 			}
@@ -421,7 +389,7 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
 				(r.StargazerCount > 0 || r.Watchers.TotalCount > 0) {
 				cs := opts.IO.ColorScheme()
 				fmt.Fprintf(opts.IO.ErrOut, "%s Changing the repository visibility to private will cause permanent loss of stars and watchers.\n", cs.WarningIcon())
-				confirmed, err = opts.Prompter.Confirm("Do you want to change visibility to private?", false)
+				confirmed, err = p.Confirm("Do you want to change visibility to private?", false)
 				if err != nil {
 					return err
 				}
@@ -441,15 +409,17 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
 			if r.RebaseMergeAllowed {
 				defaultMergeOptions = append(defaultMergeOptions, allowRebaseMerge)
 			}
-			//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-			err = prompt.SurveyAskOne(&survey.MultiSelect{
-				Message: "Allowed merge strategies",
-				Default: defaultMergeOptions,
-				Options: []string{allowMergeCommits, allowSquashMerge, allowRebaseMerge},
-			}, &selectedMergeOptions)
+			mergeOpts := []string{allowMergeCommits, allowSquashMerge, allowRebaseMerge}
+			selected, err := p.MultiSelect(
+				"Allowed merge strategies",
+				defaultMergeOptions,
+				mergeOpts)
 			if err != nil {
 				return err
 			}
+			for _, i := range selected {
+				selectedMergeOptions = append(selectedMergeOptions, mergeOpts[i])
+			}
 			enableMergeCommit := isIncluded(allowMergeCommits, selectedMergeOptions)
 			opts.Edits.EnableMergeCommit = &enableMergeCommit
 			enableSquashMerge := isIncluded(allowSquashMerge, selectedMergeOptions)
@@ -461,44 +431,33 @@ func interactiveRepoEdit(opts *EditOptions, r *api.Repository) error {
 			}
 
 			opts.Edits.EnableAutoMerge = &r.AutoMergeAllowed
-			//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-			err = prompt.SurveyAskOne(&survey.Confirm{
-				Message: "Enable Auto Merge?",
-				Default: r.AutoMergeAllowed,
-			}, opts.Edits.EnableAutoMerge)
+			c, err := p.Confirm("Enable Auto Merge?", r.AutoMergeAllowed)
 			if err != nil {
 				return err
 			}
+			opts.Edits.EnableAutoMerge = &c
 
 			opts.Edits.DeleteBranchOnMerge = &r.DeleteBranchOnMerge
-			//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-			err = prompt.SurveyAskOne(&survey.Confirm{
-				Message: "Automatically delete head branches after merging?",
-				Default: r.DeleteBranchOnMerge,
-			}, opts.Edits.DeleteBranchOnMerge)
+			c, err = p.Confirm(
+				"Automatically delete head branches after merging?", r.DeleteBranchOnMerge)
 			if err != nil {
 				return err
 			}
+			opts.Edits.DeleteBranchOnMerge = &c
 		case optionTemplateRepo:
-			opts.Edits.IsTemplate = &r.IsTemplate
-			//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-			err = prompt.SurveyAskOne(&survey.Confirm{
-				Message: "Convert into a template repository?",
-				Default: r.IsTemplate,
-			}, opts.Edits.IsTemplate)
+			c, err := p.Confirm("Convert into a template repository?", r.IsTemplate)
 			if err != nil {
 				return err
 			}
+			opts.Edits.IsTemplate = &c
 		case optionAllowForking:
-			opts.Edits.AllowForking = &r.ForkingAllowed
-			//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-			err = prompt.SurveyAskOne(&survey.Confirm{
-				Message: "Allow forking (of an organization repository)?",
-				Default: r.ForkingAllowed,
-			}, opts.Edits.AllowForking)
+			c, err := p.Confirm(
+				"Allow forking (of an organization repository)?",
+				r.ForkingAllowed)
 			if err != nil {
 				return err
 			}
+			opts.Edits.AllowForking = &c
 		}
 	}
 	return nil
diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go
index 8c8420bee..fd6378216 100644
--- a/pkg/cmd/root/root.go
+++ b/pkg/cmd/root/root.go
@@ -5,9 +5,11 @@ import (
 	"net/http"
 	"os"
 	"strings"
+	"sync"
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/api"
+	codespacesAPI "github.com/cli/cli/v2/internal/codespaces/api"
 	actionsCmd "github.com/cli/cli/v2/pkg/cmd/actions"
 	aliasCmd "github.com/cli/cli/v2/pkg/cmd/alias"
 	"github.com/cli/cli/v2/pkg/cmd/alias/shared"
@@ -132,7 +134,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command,
 	cmd.AddCommand(variableCmd.NewCmdVariable(f))
 	cmd.AddCommand(sshKeyCmd.NewCmdSSHKey(f))
 	cmd.AddCommand(statusCmd.NewCmdStatus(f, nil))
-	cmd.AddCommand(codespaceCmd.NewCmdCodespace(f))
+	cmd.AddCommand(newCodespaceCmd(f))
 	cmd.AddCommand(projectCmd.NewCmdProject(f))
 
 	// below here at the commands that require the "intelligent" BaseRepo resolver
@@ -239,3 +241,51 @@ func bareHTTPClient(f *cmdutil.Factory, version string) func() (*http.Client, er
 		return api.NewHTTPClient(opts)
 	}
 }
+
+func newCodespaceCmd(f *cmdutil.Factory) *cobra.Command {
+	serverURL := os.Getenv("GITHUB_SERVER_URL")
+	apiURL := os.Getenv("GITHUB_API_URL")
+	vscsURL := os.Getenv("INTERNAL_VSCS_TARGET_URL")
+	app := codespaceCmd.NewApp(
+		f.IOStreams,
+		f,
+		codespacesAPI.New(
+			serverURL,
+			apiURL,
+			vscsURL,
+			&lazyLoadedHTTPClient{factory: f},
+		),
+		f.Browser,
+		f.Remotes,
+	)
+	cmd := codespaceCmd.NewRootCmd(app)
+	cmd.Use = "codespace"
+	cmd.Aliases = []string{"cs"}
+	cmd.GroupID = "core"
+	return cmd
+}
+
+type lazyLoadedHTTPClient struct {
+	factory *cmdutil.Factory
+
+	httpClientMu sync.RWMutex // guards httpClient
+	httpClient   *http.Client
+}
+
+func (l *lazyLoadedHTTPClient) Do(req *http.Request) (*http.Response, error) {
+	l.httpClientMu.RLock()
+	httpClient := l.httpClient
+	l.httpClientMu.RUnlock()
+
+	if httpClient == nil {
+		var err error
+		l.httpClientMu.Lock()
+		l.httpClient, err = l.factory.HttpClient()
+		l.httpClientMu.Unlock()
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return l.httpClient.Do(req)
+}


Test patch:
diff --git a/internal/codespaces/api/api_test.go b/internal/codespaces/api/api_test.go
index 75f82c39e..5e9316fc1 100644
--- a/internal/codespaces/api/api_test.go
+++ b/internal/codespaces/api/api_test.go
@@ -3,16 +3,12 @@ package api
 import (
 	"context"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"reflect"
 	"strconv"
 	"testing"
-
-	"github.com/cli/cli/v2/internal/config"
-	"github.com/cli/cli/v2/pkg/cmdutil"
 )
 
 func generateCodespaceList(start int, end int) []*Codespace {
@@ -130,181 +126,13 @@ func createFakeCreateEndpointServer(t *testing.T, wantStatus int) *httptest.Serv
 	}))
 }
 
-func createHttpClient() (*http.Client, error) {
-	return &http.Client{}, nil
-}
-
-func TestNew_APIURL_dotcomConfig(t *testing.T) {
-	t.Setenv("GITHUB_API_URL", "")
-	cfg := &config.ConfigMock{
-		AuthenticationFunc: func() *config.AuthConfig {
-			return &config.AuthConfig{}
-		},
-	}
-	f := &cmdutil.Factory{
-		Config: func() (config.Config, error) {
-			return cfg, nil
-		},
-	}
-	api := New(f)
-
-	if api.githubAPI != "https://api.github.com" {
-		t.Fatalf("expected https://api.github.com, got %s", api.githubAPI)
-	}
-	if len(cfg.AuthenticationCalls()) != 1 {
-		t.Fatalf("API url was not pulled from the config")
-	}
-}
-
-func TestNew_APIURL_customConfig(t *testing.T) {
-	t.Setenv("GITHUB_API_URL", "")
-	cfg := &config.ConfigMock{
-		AuthenticationFunc: func() *config.AuthConfig {
-			authCfg := &config.AuthConfig{}
-			authCfg.SetDefaultHost("github.mycompany.com", "GH_HOST")
-			return authCfg
-		},
-	}
-	f := &cmdutil.Factory{
-		Config: func() (config.Config, error) {
-			return cfg, nil
-		},
-	}
-	api := New(f)
-
-	if api.githubAPI != "https://github.mycompany.com/api/v3" {
-		t.Fatalf("expected https://github.mycompany.com/api/v3, got %s", api.githubAPI)
-	}
-	if len(cfg.AuthenticationCalls()) != 1 {
-		t.Fatalf("API url was not pulled from the config")
-	}
-}
-
-func TestNew_APIURL_env(t *testing.T) {
-	t.Setenv("GITHUB_API_URL", "https://api.mycompany.com")
-	cfg := &config.ConfigMock{
-		AuthenticationFunc: func() *config.AuthConfig {
-			return &config.AuthConfig{}
-		},
-	}
-	f := &cmdutil.Factory{
-		Config: func() (config.Config, error) {
-			return cfg, nil
-		},
-	}
-	api := New(f)
-
-	if api.githubAPI != "https://api.mycompany.com" {
-		t.Fatalf("expected https://api.mycompany.com, got %s", api.githubAPI)
-	}
-	if len(cfg.AuthenticationCalls()) != 0 {
-		t.Fatalf("Configuration was checked instead of using the GITHUB_API_URL environment variable")
-	}
-}
-
-func TestNew_APIURL_dotcomFallback(t *testing.T) {
-	t.Setenv("GITHUB_API_URL", "")
-	f := &cmdutil.Factory{
-		Config: func() (config.Config, error) {
-			return nil, errors.New("Failed to load")
-		},
-	}
-	api := New(f)
-
-	if api.githubAPI != "https://api.github.com" {
-		t.Fatalf("expected https://api.github.com, got %s", api.githubAPI)
-	}
-}
-
-func TestNew_ServerURL_dotcomConfig(t *testing.T) {
-	t.Setenv("GITHUB_SERVER_URL", "")
-	cfg := &config.ConfigMock{
-		AuthenticationFunc: func() *config.AuthConfig {
-			return &config.AuthConfig{}
-		},
-	}
-	f := &cmdutil.Factory{
-		Config: func() (config.Config, error) {
-			return cfg, nil
-		},
-	}
-	api := New(f)
-
-	if api.githubServer != "https://github.com" {
-		t.Fatalf("expected https://github.com, got %s", api.githubServer)
-	}
-	if len(cfg.AuthenticationCalls()) != 1 {
-		t.Fatalf("Server url was not pulled from the config")
-	}
-}
-
-func TestNew_ServerURL_customConfig(t *testing.T) {
-	t.Setenv("GITHUB_SERVER_URL", "")
-	cfg := &config.ConfigMock{
-		AuthenticationFunc: func() *config.AuthConfig {
-			authCfg := &config.AuthConfig{}
-			authCfg.SetDefaultHost("github.mycompany.com", "GH_HOST")
-			return authCfg
-		},
-	}
-	f := &cmdutil.Factory{
-		Config: func() (config.Config, error) {
-			return cfg, nil
-		},
-	}
-	api := New(f)
-
-	if api.githubServer != "https://github.mycompany.com" {
-		t.Fatalf("expected https://github.mycompany.com, got %s", api.githubServer)
-	}
-	if len(cfg.AuthenticationCalls()) != 1 {
-		t.Fatalf("Server url was not pulled from the config")
-	}
-}
-
-func TestNew_ServerURL_env(t *testing.T) {
-	t.Setenv("GITHUB_SERVER_URL", "https://mycompany.com")
-	cfg := &config.ConfigMock{
-		AuthenticationFunc: func() *config.AuthConfig {
-			return &config.AuthConfig{}
-		},
-	}
-	f := &cmdutil.Factory{
-		Config: func() (config.Config, error) {
-			return cfg, nil
-		},
-	}
-	api := New(f)
-
-	if api.githubServer != "https://mycompany.com" {
-		t.Fatalf("expected https://mycompany.com, got %s", api.githubServer)
-	}
-	if len(cfg.AuthenticationCalls()) != 0 {
-		t.Fatalf("Configuration was checked instead of using the GITHUB_SERVER_URL environment variable")
-	}
-}
-
-func TestNew_ServerURL_dotcomFallback(t *testing.T) {
-	t.Setenv("GITHUB_SERVER_URL", "")
-	f := &cmdutil.Factory{
-		Config: func() (config.Config, error) {
-			return nil, errors.New("Failed to load")
-		},
-	}
-	api := New(f)
-
-	if api.githubServer != "https://github.com" {
-		t.Fatalf("expected https://github.com, got %s", api.githubServer)
-	}
-}
-
 func TestCreateCodespaces(t *testing.T) {
 	svr := createFakeCreateEndpointServer(t, http.StatusCreated)
 	defer svr.Close()
 
 	api := API{
 		githubAPI: svr.URL,
-		client:    createHttpClient,
+		client:    &http.Client{},
 	}
 
 	ctx := context.TODO()
@@ -333,7 +161,7 @@ func TestCreateCodespaces_displayName(t *testing.T) {
 
 	api := API{
 		githubAPI: svr.URL,
-		client:    createHttpClient,
+		client:    &http.Client{},
 	}
 
 	retentionPeriod := 0
@@ -358,7 +186,7 @@ func TestCreateCodespaces_Pending(t *testing.T) {
 
 	api := API{
 		githubAPI:    svr.URL,
-		client:       createHttpClient,
+		client:       &http.Client{},
 		retryBackoff: 0,
 	}
 
@@ -385,7 +213,7 @@ func TestListCodespaces_limited(t *testing.T) {
 
 	api := API{
 		githubAPI: svr.URL,
-		client:    createHttpClient,
+		client:    &http.Client{},
 	}
 	ctx := context.TODO()
 	codespaces, err := api.ListCodespaces(ctx, ListCodespacesOptions{Limit: 200})
@@ -410,7 +238,7 @@ func TestListCodespaces_unlimited(t *testing.T) {
 
 	api := API{
 		githubAPI: svr.URL,
-		client:    createHttpClient,
+		client:    &http.Client{},
 	}
 	ctx := context.TODO()
 	codespaces, err := api.ListCodespaces(ctx, ListCodespacesOptions{})
@@ -501,7 +329,7 @@ func runRepoSearchTest(t *testing.T, searchText, wantQueryText, wantSort, wantMa
 
 	api := API{
 		githubAPI: svr.URL,
-		client:    createHttpClient,
+		client:    &http.Client{},
 	}
 
 	ctx := context.Background()
@@ -546,7 +374,7 @@ func TestRetries(t *testing.T) {
 	t.Cleanup(srv.Close)
 	a := &API{
 		githubAPI: srv.URL,
-		client:    createHttpClient,
+		client:    &http.Client{},
 	}
 	cs, err := a.GetCodespace(context.Background(), "test", false)
 	if err != nil {
@@ -734,7 +562,7 @@ func TestAPI_EditCodespace(t *testing.T) {
 			defer svr.Close()
 
 			a := &API{
-				client:    createHttpClient,
+				client:    &http.Client{},
 				githubAPI: svr.URL,
 			}
 			got, err := a.EditCodespace(tt.args.ctx, tt.args.codespaceName, tt.args.params)
@@ -774,7 +602,7 @@ func TestAPI_EditCodespacePendingOperation(t *testing.T) {
 	defer svr.Close()
 
 	a := &API{
-		client:    createHttpClient,
+		client:    &http.Client{},
 		githubAPI: svr.URL,
 	}
 
diff --git a/pkg/cmd/codespace/create_test.go b/pkg/cmd/codespace/create_test.go
index caf6df271..7b74c206d 100644
--- a/pkg/cmd/codespace/create_test.go
+++ b/pkg/cmd/codespace/create_test.go
@@ -453,32 +453,11 @@ Alternatively, you can run "create" with the "--default-permissions" option to c
 		},
 		{
 			name: "return default url when using web flag without other flags",
-			fields: fields{
-				apiClient: apiCreateDefaults(&apiClientMock{
-					ServerURLFunc: func() string {
-						return "https://github.com"
-					},
-				}),
-			},
 			opts: createOptions{
 				useWeb: true,
 			},
 			wantURL: "https://github.com/codespaces/new",
 		},
-		{
-			name: "return custom server url when using web flag",
-			fields: fields{
-				apiClient: apiCreateDefaults(&apiClientMock{
-					ServerURLFunc: func() string {
-						return "https://github.mycompany.com"
-					},
-				}),
-			},
-			opts: createOptions{
-				useWeb: true,
-			},
-			wantURL: "https://github.mycompany.com/codespaces/new",
-		},
 		{
 			name: "skip machine check when using web flag and no machine provided",
 			fields: fields{
@@ -494,9 +473,6 @@ Alternatively, you can run "create" with the "--default-permissions" option to c
 							Name: "monalisa-dotfiles-abcd1234",
 						}, nil
 					},
-					ServerURLFunc: func() string {
-						return "https://github.com"
-					},
 				}),
 			},
 			opts: createOptions{
@@ -523,9 +499,6 @@ Alternatively, you can run "create" with the "--default-permissions" option to c
 							Name: "monalisa-dotfiles-abcd1234",
 						}, nil
 					},
-					ServerURLFunc: func() string {
-						return "https://github.com"
-					},
 				}),
 			},
 			opts: createOptions{
@@ -551,9 +524,6 @@ Alternatively, you can run "create" with the "--default-permissions" option to c
 							Machine: api.CodespaceMachine{Name: "GIGA"},
 						}, nil
 					},
-					ServerURLFunc: func() string {
-						return "https://github.com"
-					},
 				}),
 			},
 			opts: createOptions{
diff --git a/pkg/cmd/codespace/list_test.go b/pkg/cmd/codespace/list_test.go
index 35f30581b..d99b8b97e 100644
--- a/pkg/cmd/codespace/list_test.go
+++ b/pkg/cmd/codespace/list_test.go
@@ -170,32 +170,11 @@ func TestApp_List(t *testing.T) {
 		},
 		{
 			name: "list codespaces,--web",
-			fields: fields{
-				apiClient: &apiClientMock{
-					ServerURLFunc: func() string {
-						return "https://github.com"
-					},
-				},
-			},
 			opts: &listOptions{
 				useWeb: true,
 			},
 			wantURL: "https://github.com/codespaces",
 		},
-		{
-			name: "list codespaces,--web with custom server url",
-			fields: fields{
-				apiClient: &apiClientMock{
-					ServerURLFunc: func() string {
-						return "https://github.mycompany.com"
-					},
-				},
-			},
-			opts: &listOptions{
-				useWeb: true,
-			},
-			wantURL: "https://github.mycompany.com/codespaces",
-		},
 		{
 			name: "list codespaces,--web, --repo flag",
 			fields: fields{
@@ -220,9 +199,6 @@ func TestApp_List(t *testing.T) {
 							},
 						}, nil
 					},
-					ServerURLFunc: func() string {
-						return "https://github.com"
-					},
 				},
 			},
 			opts: &listOptions{
diff --git a/pkg/cmd/project/close/close_test.go b/pkg/cmd/project/close/close_test.go
index 96ec6ac50..a19dcdabd 100644
--- a/pkg/cmd/project/close/close_test.go
+++ b/pkg/cmd/project/close/close_test.go
@@ -1,6 +1,7 @@
 package close
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -55,7 +56,8 @@ func TestNewCmdClose(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/copy/copy_test.go b/pkg/cmd/project/copy/copy_test.go
index 3caec592d..d8abf5ec9 100644
--- a/pkg/cmd/project/copy/copy_test.go
+++ b/pkg/cmd/project/copy/copy_test.go
@@ -1,6 +1,7 @@
 package copy
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -74,7 +75,8 @@ func TestNewCmdCopy(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/create/create_test.go b/pkg/cmd/project/create/create_test.go
index 74d1747b7..3ef773cb9 100644
--- a/pkg/cmd/project/create/create_test.go
+++ b/pkg/cmd/project/create/create_test.go
@@ -1,6 +1,7 @@
 package create
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -44,7 +45,8 @@ func TestNewCmdCreate(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/delete/delete_test.go b/pkg/cmd/project/delete/delete_test.go
index 589ef72bc..a27180266 100644
--- a/pkg/cmd/project/delete/delete_test.go
+++ b/pkg/cmd/project/delete/delete_test.go
@@ -1,6 +1,7 @@
 package delete
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -48,7 +49,8 @@ func TestNewCmdDelete(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/edit/edit_test.go b/pkg/cmd/project/edit/edit_test.go
index da30b0935..605c5f5cc 100644
--- a/pkg/cmd/project/edit/edit_test.go
+++ b/pkg/cmd/project/edit/edit_test.go
@@ -1,6 +1,7 @@
 package edit
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -91,7 +92,8 @@ func TestNewCmdEdit(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/field-create/field_create_test.go b/pkg/cmd/project/field-create/field_create_test.go
index 01904d9e9..31328a04f 100644
--- a/pkg/cmd/project/field-create/field_create_test.go
+++ b/pkg/cmd/project/field-create/field_create_test.go
@@ -1,6 +1,7 @@
 package fieldcreate
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -78,7 +79,8 @@ func TestNewCmdCreateField(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/field-delete/field_delete_test.go b/pkg/cmd/project/field-delete/field_delete_test.go
index 6b1e6c763..61783d44d 100644
--- a/pkg/cmd/project/field-delete/field_delete_test.go
+++ b/pkg/cmd/project/field-delete/field_delete_test.go
@@ -1,6 +1,7 @@
 package fielddelete
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -42,7 +43,8 @@ func TestNewCmdDeleteField(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/field-list/field_list_test.go b/pkg/cmd/project/field-list/field_list_test.go
index c6c2f97c6..60e61188d 100644
--- a/pkg/cmd/project/field-list/field_list_test.go
+++ b/pkg/cmd/project/field-list/field_list_test.go
@@ -1,6 +1,7 @@
 package fieldlist
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/internal/tableprinter"
@@ -52,7 +53,8 @@ func TestNewCmdList(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/item-add/item_add_test.go b/pkg/cmd/project/item-add/item_add_test.go
index 0beb1fed6..4f1ebd4f6 100644
--- a/pkg/cmd/project/item-add/item_add_test.go
+++ b/pkg/cmd/project/item-add/item_add_test.go
@@ -1,6 +1,7 @@
 package itemadd
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -64,7 +65,8 @@ func TestNewCmdaddItem(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/item-archive/item_archive_test.go b/pkg/cmd/project/item-archive/item_archive_test.go
index 6eb8336f2..85a34af22 100644
--- a/pkg/cmd/project/item-archive/item_archive_test.go
+++ b/pkg/cmd/project/item-archive/item_archive_test.go
@@ -1,6 +1,7 @@
 package itemarchive
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -72,7 +73,8 @@ func TestNewCmdarchiveItem(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/item-create/item_create_test.go b/pkg/cmd/project/item-create/item_create_test.go
index 1482fe268..d49144f3c 100644
--- a/pkg/cmd/project/item-create/item_create_test.go
+++ b/pkg/cmd/project/item-create/item_create_test.go
@@ -1,6 +1,7 @@
 package itemcreate
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -72,7 +73,8 @@ func TestNewCmdCreateItem(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/item-delete/item_delete_test.go b/pkg/cmd/project/item-delete/item_delete_test.go
index 4d3e97059..c5a3f01fd 100644
--- a/pkg/cmd/project/item-delete/item_delete_test.go
+++ b/pkg/cmd/project/item-delete/item_delete_test.go
@@ -1,6 +1,7 @@
 package itemdelete
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -64,7 +65,8 @@ func TestNewCmdDeleteItem(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/item-edit/item_edit_test.go b/pkg/cmd/project/item-edit/item_edit_test.go
index 5cdc69b87..156871f32 100644
--- a/pkg/cmd/project/item-edit/item_edit_test.go
+++ b/pkg/cmd/project/item-edit/item_edit_test.go
@@ -1,6 +1,7 @@
 package itemedit
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -104,7 +105,8 @@ func TestNewCmdeditItem(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/item-list/item_list_test.go b/pkg/cmd/project/item-list/item_list_test.go
index 490473691..d618451b1 100644
--- a/pkg/cmd/project/item-list/item_list_test.go
+++ b/pkg/cmd/project/item-list/item_list_test.go
@@ -1,6 +1,7 @@
 package itemlist
 
 import (
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -53,7 +54,8 @@ func TestNewCmdList(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/list/list_test.go b/pkg/cmd/project/list/list_test.go
index 492d224b0..48fb689ed 100644
--- a/pkg/cmd/project/list/list_test.go
+++ b/pkg/cmd/project/list/list_test.go
@@ -2,6 +2,7 @@ package list
 
 import (
 	"bytes"
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/internal/tableprinter"
@@ -55,7 +56,8 @@ func TestNewCmdlist(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/view/view_test.go b/pkg/cmd/project/view/view_test.go
index 35f6dfc9b..5c76c1413 100644
--- a/pkg/cmd/project/view/view_test.go
+++ b/pkg/cmd/project/view/view_test.go
@@ -2,6 +2,7 @@ package view
 
 import (
 	"bytes"
+	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -56,7 +57,8 @@ func TestNewCmdview(t *testing.T) {
 		},
 	}
 
-	t.Setenv("GH_TOKEN", "auth-token")
+	os.Setenv("GH_TOKEN", "auth-token")
+	defer os.Unsetenv("GH_TOKEN")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/repo/edit/edit_test.go b/pkg/cmd/repo/edit/edit_test.go
index 7302b5169..07f7d4d55 100644
--- a/pkg/cmd/repo/edit/edit_test.go
+++ b/pkg/cmd/repo/edit/edit_test.go
@@ -7,9 +7,8 @@ import (
 	"net/http"
 	"testing"
 
-	"github.com/cli/cli/v2/pkg/prompt"
-
 	"github.com/cli/cli/v2/internal/ghrepo"
+	"github.com/cli/cli/v2/internal/prompter"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/httpmock"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -175,23 +174,163 @@ func Test_editRun(t *testing.T) {
 }
 
 func Test_editRun_interactive(t *testing.T) {
+	editList := []string{
+		"Default Branch Name",
+		"Description",
+		"Home Page URL",
+		"Issues",
+		"Merge Options",
+		"Projects",
+		"Template Repository",
+		"Topics",
+		"Visibility",
+		"Wikis"}
+
 	tests := []struct {
 		name        string
 		opts        EditOptions
-		askStubs    func(*prompt.AskStubber)
+		promptStubs func(*prompter.MockPrompter)
 		httpStubs   func(*testing.T, *httpmock.Registry)
 		wantsStderr string
 		wantsErr    string
 	}{
+		{
+			name: "forking of org repo",
+			opts: EditOptions{
+				Repository:      ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
+				InteractiveMode: true,
+			},
+			promptStubs: func(pm *prompter.MockPrompter) {
+				el := append(editList, optionAllowForking)
+				pm.RegisterMultiSelect("What do you want to edit?", nil, el,
+					func(_ string, _, opts []string) ([]int, error) {
+						return []int{10}, nil
+					})
+				pm.RegisterConfirm("Allow forking (of an organization repository)?", func(_ string, _ bool) (bool, error) {
+					return true, nil
+				})
+			},
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.GraphQL(`query RepositoryInfo\b`),
+					httpmock.StringResponse(`
+					{
+						"data": {
+							"repository": {
+								"visibility": "public",
+								"description": "description",
+								"homePageUrl": "https://url.com",
+								"defaultBranchRef": {
+									"name": "main"
+								},
+								"isInOrganization": true,
+								"repositoryTopics": {
+									"nodes": [{
+										"topic": {
+											"name": "x"
+										}
+									}]
+								}
+							}
+						}
+					}`))
+				reg.Register(
+					httpmock.REST("PATCH", "repos/OWNER/REPO"),
+					httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) {
+						assert.Equal(t, true, payload["allow_forking"])
+					}))
+			},
+		},
+		{
+			name: "the rest",
+			opts: EditOptions{
+				Repository:      ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
+				InteractiveMode: true,
+			},
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterMultiSelect("What do you want to edit?", nil, editList,
+					func(_ string, _, opts []string) ([]int, error) {
+						return []int{0, 2, 3, 5, 6, 8, 9}, nil
+					})
+				pm.RegisterInput("Default branch name", func(_, _ string) (string, error) {
+					return "trunk", nil
+				})
+				pm.RegisterInput("Repository home page URL", func(_, _ string) (string, error) {
+					return "https://zombo.com", nil
+				})
+				pm.RegisterConfirm("Enable Issues?", func(_ string, _ bool) (bool, error) {
+					return true, nil
+				})
+				pm.RegisterConfirm("Enable Projects?", func(_ string, _ bool) (bool, error) {
+					return true, nil
+				})
+				pm.RegisterConfirm("Convert into a template repository?", func(_ string, _ bool) (bool, error) {
+					return true, nil
+				})
+				pm.RegisterSelect("Visibility", []string{"public", "private", "internal"},
+					func(_, _ string, opts []string) (int, error) {
+						return prompter.IndexFor(opts, "private")
+					})
+				pm.RegisterConfirm("Do you want to change visibility to private?", func(_ string, _ bool) (bool, error) {
+					return true, nil
+				})
+				pm.RegisterConfirm("Enable Wikis?", func(_ string, _ bool) (bool, error) {
+					return true, nil
+				})
+			},
+			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
+				reg.Register(
+					httpmock.GraphQL(`query RepositoryInfo\b`),
+					httpmock.StringResponse(`
+					{
+						"data": {
+							"repository": {
+								"visibility": "public",
+								"description": "description",
+								"homePageUrl": "https://url.com",
+								"defaultBranchRef": {
+									"name": "main"
+								},
+								"stargazerCount": 10,
+								"isInOrganization": false,
+								"repositoryTopics": {
+									"nodes": [{
+										"topic": {
+											"name": "x"
+										}
+									}]
+								}
+							}
+						}
+					}`))
+				reg.Register(
+					httpmock.REST("PATCH", "repos/OWNER/REPO"),
+					httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) {
+						assert.Equal(t, "trunk", payload["default_branch"])
+						assert.Equal(t, "https://zombo.com", payload["homepage"])
+						assert.Equal(t, true, payload["has_issues"])
+						assert.Equal(t, true, payload["has_projects"])
+						assert.Equal(t, "private", payload["visibility"])
+						assert.Equal(t, true, payload["is_template"])
+						assert.Equal(t, true, payload["has_wiki"])
+					}))
+			},
+		},
 		{
 			name: "updates repo description",
 			opts: EditOptions{
 				Repository:      ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
 				InteractiveMode: true,
 			},
-			askStubs: func(as *prompt.AskStubber) {
-				as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Description"})
-				as.StubPrompt("Description of the repository").AnswerWith("awesome repo description")
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterMultiSelect("What do you want to edit?", nil, editList,
+					func(_ string, _, opts []string) ([]int, error) {
+						return []int{1}, nil
+					})
+				pm.RegisterInput("Description of the repository",
+					func(_, _ string) (string, error) {
+						return "awesome repo description", nil
+					})
 			},
 			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
 				reg.Register(
@@ -229,11 +368,23 @@ func Test_editRun_interactive(t *testing.T) {
 				Repository:      ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
 				InteractiveMode: true,
 			},
-			askStubs: func(as *prompt.AskStubber) {
-				as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Description", "Topics"})
-				as.StubPrompt("Description of the repository").AnswerWith("awesome repo description")
-				as.StubPrompt("Add topics?(csv format)").AnswerWith("a, b,c,d ")
-				as.StubPrompt("Remove Topics").AnswerWith([]string{"x"})
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterMultiSelect("What do you want to edit?", nil, editList,
+					func(_ string, _, opts []string) ([]int, error) {
+						return []int{1, 7}, nil
+					})
+				pm.RegisterInput("Description of the repository",
+					func(_, _ string) (string, error) {
+						return "awesome repo description", nil
+					})
+				pm.RegisterInput("Add topics?(csv format)",
+					func(_, _ string) (string, error) {
+						return "a, b,c,d ", nil
+					})
+				pm.RegisterMultiSelect("Remove Topics", nil, []string{"x"},
+					func(_ string, _, opts []string) ([]int, error) {
+						return []int{0}, nil
+					})
 			},
 			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
 				reg.Register(
@@ -276,11 +427,22 @@ func Test_editRun_interactive(t *testing.T) {
 				Repository:      ghrepo.NewWithHost("OWNER", "REPO", "github.com"),
 				InteractiveMode: true,
 			},
-			askStubs: func(as *prompt.AskStubber) {
-				as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Merge Options"})
-				as.StubPrompt("Allowed merge strategies").AnswerWith([]string{allowMergeCommits, allowRebaseMerge})
-				as.StubPrompt("Enable Auto Merge?").AnswerWith(false)
-				as.StubPrompt("Automatically delete head branches after merging?").AnswerWith(false)
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterMultiSelect("What do you want to edit?", nil, editList,
+					func(_ string, _, opts []string) ([]int, error) {
+						return []int{4}, nil
+					})
+				pm.RegisterMultiSelect("Allowed merge strategies", nil,
+					[]string{allowMergeCommits, allowSquashMerge, allowRebaseMerge},
+					func(_ string, _, opts []string) ([]int, error) {
+						return []int{0, 2}, nil
+					})
+				pm.RegisterConfirm("Enable Auto Merge?", func(_ string, _ bool) (bool, error) {
+					return false, nil
+				})
+				pm.RegisterConfirm("Automatically delete head branches after merging?", func(_ string, _ bool) (bool, error) {
+					return false, nil
+				})
 			},
 			httpStubs: func(t *testing.T, reg *httpmock.Registry) {
 				reg.Register(
@@ -333,14 +495,15 @@ func Test_editRun_interactive(t *testing.T) {
 				tt.httpStubs(t, httpReg)
 			}
 
+			pm := prompter.NewMockPrompter(t)
+			tt.opts.Prompter = pm
+			if tt.promptStubs != nil {
+				tt.promptStubs(pm)
+			}
+
 			opts := &tt.opts
 			opts.HTTPClient = &http.Client{Transport: httpReg}
 			opts.IO = ios
-			//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
-			as := prompt.NewAskStubber(t)
-			if tt.askStubs != nil {
-				tt.askStubs(as)
-			}
 
 			err := editRun(context.Background(), opts)
 			if tt.wantsErr == "" {