diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go index 1dff0fba9..7d5c42909 100644 --- a/internal/codespaces/api/api.go +++ b/internal/codespaces/api/api.go @@ -347,6 +347,30 @@ func (a *API) StartCodespace(ctx context.Context, codespaceName string) error { return nil } +func (a *API) StopCodespace(ctx context.Context, codespaceName string) error { + req, err := http.NewRequest( + http.MethodPost, + a.githubAPI+"/user/codespaces/"+codespaceName+"/stop", + nil, + ) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + a.setHeaders(req) + resp, err := a.do(ctx, req, "/user/codespaces/*/stop") + if err != nil { + return fmt.Errorf("error making request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return api.HandleHTTPError(resp) + } + + return nil +} + type getCodespaceRegionLocationResponse struct { Current string `json:"current"` } diff --git a/pkg/cmd/codespace/common.go b/pkg/cmd/codespace/common.go index b93b9a8d6..e86e64439 100644 --- a/pkg/cmd/codespace/common.go +++ b/pkg/cmd/codespace/common.go @@ -38,6 +38,7 @@ type apiClient interface { ListCodespaces(ctx context.Context, limit int) ([]*api.Codespace, error) DeleteCodespace(ctx context.Context, name string) error StartCodespace(ctx context.Context, name string) error + StopCodespace(ctx context.Context, name string) error CreateCodespace(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) GetRepository(ctx context.Context, nwo string) (*api.Repository, error) AuthorizedKeys(ctx context.Context, user string) ([]byte, error) @@ -251,3 +252,7 @@ func (c codespace) branchWithGitStatus() string { func (c codespace) hasUnsavedChanges() bool { return c.Environment.GitStatus.HasUncommitedChanges || c.Environment.GitStatus.HasUnpushedChanges } + +func (c codespace) isRunning() bool { + return c.Environment.State == api.CodespaceEnvironmentStateAvailable +} diff --git a/pkg/cmd/codespace/root.go b/pkg/cmd/codespace/root.go index bd566bb5a..efc1c763e 100644 --- a/pkg/cmd/codespace/root.go +++ b/pkg/cmd/codespace/root.go @@ -25,6 +25,7 @@ token to access the GitHub API with.`, root.AddCommand(newLogsCmd(app)) root.AddCommand(newPortsCmd(app)) root.AddCommand(newSSHCmd(app)) + root.AddCommand(newStopCmd(app)) return root } diff --git a/pkg/cmd/codespace/stop.go b/pkg/cmd/codespace/stop.go new file mode 100644 index 000000000..b123204f9 --- /dev/null +++ b/pkg/cmd/codespace/stop.go @@ -0,0 +1,68 @@ +package codespace + +import ( + "context" + "errors" + "fmt" + + "github.com/cli/cli/v2/internal/codespaces/api" + "github.com/spf13/cobra" +) + +func newStopCmd(app *App) *cobra.Command { + var codespace string + + stopCmd := &cobra.Command{ + Use: "stop", + Short: "Stop a running codespace", + Args: noArgsConstraint, + RunE: func(cmd *cobra.Command, args []string) error { + return app.StopCodespace(cmd.Context(), codespace) + }, + } + stopCmd.Flags().StringVarP(&codespace, "codespace", "c", "", "Name of the codespace") + + return stopCmd +} + +func (a *App) StopCodespace(ctx context.Context, codespaceName string) error { + if codespaceName == "" { + codespaces, err := a.apiClient.ListCodespaces(ctx, -1) + if err != nil { + return fmt.Errorf("failed to list codespace: %w", err) + } + + var runningCodespaces []*api.Codespace + for _, c := range codespaces { + cs := codespace{c} + if cs.isRunning() { + runningCodespaces = append(runningCodespaces, c) + } + } + if len(runningCodespaces) == 0 { + return errors.New("no running codespaces") + } + + codespace, err := chooseCodespaceFromList(ctx, runningCodespaces) + if err != nil { + return fmt.Errorf("failed to choose codespace: %w", err) + } + codespaceName = codespace.Name + } else { + c, err := a.apiClient.GetCodespace(ctx, codespaceName, false) + if err != nil { + return fmt.Errorf("failed to get codespace: %w", err) + } + cs := codespace{c} + if !cs.isRunning() { + return fmt.Errorf("codespace %q is not running", codespaceName) + } + } + + if err := a.apiClient.StopCodespace(ctx, codespaceName); err != nil { + return fmt.Errorf("failed to stop codespace: %w", err) + } + a.logger.Println("Codespace stopped") + + return nil +}