Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cmd/cloudx/client/command_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ const (
ProjectAPIKey = "ORY_PROJECT_API_KEY"
)

var ErrProjectNotSet = fmt.Errorf("no project was specified")
var (
ErrProjectNotSet = fmt.Errorf("no project was specified")
ErrWorkspaceNotSet = fmt.Errorf("no workspace was specified")
)

type (
CommandHelper struct {
Expand Down
2 changes: 2 additions & 0 deletions cmd/cloudx/use.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/ory/cli/cmd/cloudx/client"
"github.com/ory/cli/cmd/cloudx/project"
"github.com/ory/cli/cmd/cloudx/workspace"
"github.com/ory/x/cmdx"
)

Expand All @@ -19,6 +20,7 @@ func NewUseCmd() *cobra.Command {

cmd.AddCommand(
project.NewUseProjectCmd(),
workspace.NewUseWorkspaceCmd(),
)

client.RegisterConfigFlag(cmd.PersistentFlags())
Expand Down
20 changes: 20 additions & 0 deletions cmd/cloudx/workspace/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,23 @@ func (o outputWorkspaces) Interface() interface{} {
func (o outputWorkspaces) Len() int {
return len(o)
}

// selectedWorkspace is the output of `ory use workspace`, mirroring the shape
// `ory use project` prints.
type selectedWorkspace struct {
ID string `json:"id"`
}

var _ cmdx.TableRow = (*selectedWorkspace)(nil)

func (*selectedWorkspace) Header() []string {
return []string{"ID"}
}

func (i *selectedWorkspace) Columns() []string {
return []string{i.ID}
}

func (i *selectedWorkspace) Interface() interface{} {
return i
}
62 changes: 62 additions & 0 deletions cmd/cloudx/workspace/use.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package workspace

import (
"github.com/spf13/cobra"

"github.com/ory/cli/cmd/cloudx/client"
"github.com/ory/x/cmdx"
)

func NewUseWorkspaceCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "workspace [id]",
Aliases: []string{"workspaces", "ws"},
Args: cobra.MaximumNArgs(1),
Short: "Set the workspace as the default. When no id is provided, prints the currently used default workspace.",
Example: `$ ory use workspace ecaaa3cb-0730-4ee8-a6df-9553cdfeef89

ID ecaaa3cb-0730-4ee8-a6df-9553cdfeef89

$ ory use workspace ecaaa3cb-0730-4ee8-a6df-9553cdfeef89 --format json

{
"id": "ecaaa3cb-0730-4ee8-a6df-9553cdfeef89"
}`,
RunE: func(cmd *cobra.Command, args []string) error {
opts := make([]client.CommandHelperOption, 0, 1)
if len(args) == 1 {
opts = append(opts, client.WithWorkspaceOverride(args[0]))
}
h, err := client.NewCobraCommandHelper(cmd, opts...)
if err != nil {
return err
}

// The helper resolves a (partial) name or ID to the workspace ID,
// and falls back to the environment or the one already stored in
// the config when no argument is given.
id := h.WorkspaceID()
if id == nil {
return client.ErrWorkspaceNotSet
}

// Only persist when the user asked to change the default. Without
// an argument this command just reports the current one, which must
// not overwrite the stored default with e.g. an ORY_WORKSPACE value.
if len(args) == 1 {
if err := h.SelectWorkspace(*id); err != nil {
return cmdx.PrintOpenAPIError(cmd, err)
}
}

cmdx.PrintRow(cmd, &selectedWorkspace{ID: *id})
return nil
},
}

cmdx.RegisterFormatFlags(cmd.Flags())
return cmd
}
113 changes: 113 additions & 0 deletions cmd/cloudx/workspace/use_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package workspace_test

import (
"context"
"encoding/json"
"os"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ory/cli/cmd/cloudx/client"
"github.com/ory/cli/cmd/cloudx/testhelpers"
)

// TestUseWorkspace covers https://github.com/ory/cli/issues/406. The command is
// exercised against a config file only, so it needs no Ory Network access:
// selecting an already-known workspace ID never hits the API.
func TestUseWorkspace(t *testing.T) {
t.Parallel()

const (
initial = "11111111-1111-1111-1111-111111111111"
other = "33333333-3333-3333-3333-333333333333"
project = "22222222-2222-2222-2222-222222222222"
accessToken = "the-access-token"
)

newContext := func(t *testing.T, workspace string) (context.Context, string) {
t.Helper()

conf := map[string]any{
"version": client.ConfigVersion,
"selected_project": project,
"access_token": map[string]any{"access_token": accessToken, "token_type": "bearer"},
}
if workspace != "" {
conf["selected_workspace"] = workspace
}
raw, err := json.Marshal(conf)
require.NoError(t, err)

location := testhelpers.NewConfigFile(t)
require.NoError(t, os.WriteFile(location, raw, 0600))

return client.ContextWithOptions(context.Background(), client.WithConfigLocation(location)), location
}

t.Run("case=prints the default workspace when no id is given", func(t *testing.T) {
t.Parallel()

ctx, _ := newContext(t, initial)

stdout, _, err := testhelpers.Cmd(ctx).Exec(nil, "use", "workspace", "--quiet")
require.NoError(t, err)
assert.Equal(t, initial, strings.TrimSpace(stdout))
})

t.Run("case=prints the default workspace as JSON", func(t *testing.T) {
t.Parallel()

ctx, _ := newContext(t, initial)

stdout, _, err := testhelpers.Cmd(ctx).Exec(nil, "use", "workspace", "--format", "json")
require.NoError(t, err)
assert.JSONEq(t, `{"id":"`+initial+`"}`, stdout)
})

t.Run("case=does not persist an overridden workspace when no id is given", func(t *testing.T) {
t.Parallel()

ctx, location := newContext(t, initial)
// Simulates ORY_WORKSPACE (or a workspace API key) pointing somewhere
// else: printing the default must not rewrite it.
ctx = client.ContextWithOptions(ctx, client.WithWorkspaceOverride(other))

stdout, _, err := testhelpers.Cmd(ctx).Exec(nil, "use", "workspace", "--quiet")
require.NoError(t, err)
assert.Equal(t, other, strings.TrimSpace(stdout))

conf := testhelpers.ReadConfig(t, location)
assert.Equal(t, initial, conf.SelectedWorkspace.String())
})

t.Run("case=sets the default workspace and persists it", func(t *testing.T) {
t.Parallel()

ctx, location := newContext(t, initial)

stdout, _, err := testhelpers.Cmd(ctx).Exec(nil, "use", "workspace", other, "--quiet")
require.NoError(t, err)
assert.Equal(t, other, strings.TrimSpace(stdout))

conf := testhelpers.ReadConfig(t, location)
assert.Equal(t, other, conf.SelectedWorkspace.String())
assert.Equal(t, project, conf.SelectedProject.String(), "selecting a workspace must not clear the project")
require.NotNil(t, conf.AccessToken, "selecting a workspace must not log the user out")
assert.Equal(t, accessToken, conf.AccessToken.AccessToken)
})

t.Run("case=errors when no workspace is set", func(t *testing.T) {
t.Parallel()

ctx, _ := newContext(t, "")

_, _, err := testhelpers.Cmd(ctx).Exec(nil, "use", "workspace", "--quiet")
assert.ErrorIs(t, err, client.ErrWorkspaceNotSet)
})
}
Loading